python-agent-harness 1.5.6.0__tar.gz → 1.5.6.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.6.2/PKG-INFO +239 -0
- python_agent_harness-1.5.6.2/README.md +205 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/pyproject.toml +1 -1
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/core/agent.py +90 -9
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/core/models.py +33 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/core/tool_runner.py +12 -2
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/entry/cli.py +58 -11
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/entry/controller.py +15 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/entry/headless.py +216 -23
- python_agent_harness-1.5.6.2/python_agent_harness/entry/server.py +996 -0
- python_agent_harness-1.5.6.2/python_agent_harness/entry/view.py +110 -0
- python_agent_harness-1.5.6.2/python_agent_harness/io/text_filter.py +262 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/llm/client.py +22 -2
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/agent.md +17 -36
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/subagent.md +0 -2
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/session/config.py +48 -11
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/session/session.py +76 -17
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/base.py +14 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/bash.py +7 -1
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/edit.py +2 -2
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/glob.py +2 -2
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/glob_mac.py +2 -2
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/grep.py +7 -2
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/grep_mac.py +2 -2
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/insert.py +2 -4
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/lsp.py +2 -9
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/mkdir.py +4 -2
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/read.py +2 -3
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/write.py +5 -6
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tui/commands.py +43 -34
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tui/core.py +80 -21
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tui/input.py +6 -4
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tui/render.py +78 -42
- python_agent_harness-1.5.6.2/python_agent_harness.egg-info/PKG-INFO +239 -0
- python_agent_harness-1.5.6.0/PKG-INFO +0 -407
- python_agent_harness-1.5.6.0/README.md +0 -373
- python_agent_harness-1.5.6.0/python_agent_harness/entry/server.py +0 -534
- python_agent_harness-1.5.6.0/python_agent_harness/entry/view.py +0 -51
- python_agent_harness-1.5.6.0/python_agent_harness/io/text_filter.py +0 -84
- python_agent_harness-1.5.6.0/python_agent_harness.egg-info/PKG-INFO +0 -407
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/LICENSE +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/core/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/core/context_manager.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/core/planmode.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/core/subagent.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/core/token_estimator.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/entry/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/io/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/io/attachments.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/io/diffrender.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/io/persistence.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/llm/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/lsp/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/lsp/client.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/lsp/config.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/lsp/manager.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/agents/reviewer.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/core.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/session/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/session/commands.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/diffapply.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness.egg-info/SOURCES.txt +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/setup.cfg +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/tests/test_invariants.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/tests/test_scenarios.py +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-agent-harness
|
|
3
|
+
Version: 1.5.6.2
|
|
4
|
+
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
|
+
Author-email: Huming Chen <chenhuming@gmail.com>
|
|
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
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: rich>=13.0
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: certifi>=2024.0
|
|
23
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Requires-Dist: mcp<3,>=2.0; extra == "mcp"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
28
|
+
Requires-Dist: pyright>=1.1.380; extra == "dev"
|
|
29
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
30
|
+
Requires-Dist: coverage[toml]>=7.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
32
|
+
Requires-Dist: mcp<3,>=2.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
<div align="center">
|
|
36
|
+
|
|
37
|
+
# python-agent-harness
|
|
38
|
+
|
|
39
|
+
**A lightweight, hackable mini-OpenCode written in Python.**
|
|
40
|
+
FSM-driven execution · OpenAI-compatible · built for daily use and easy customization
|
|
41
|
+
|
|
42
|
+
[](https://github.com/beacoder/python-agent-harness/actions/workflows/ci.yml)
|
|
43
|
+
[](https://pypi.org/project/python-agent-harness/)
|
|
44
|
+
[](https://www.python.org/downloads/)
|
|
45
|
+
[](https://pepy.tech/projects/python-agent-harness)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
A terminal coding agent that reads your codebase, plans changes, edits files, runs commands, and verifies its work.
|
|
51
|
+
|
|
52
|
+
Inspired by [gptel-agent-harness](https://github.com/beacoder/gptel-agent-harness) and [opencode](https://github.com/anomalyco/opencode). It brings opencode's prompts and core behaviors — `AGENTS.md` discovery, plan/build modes, skills, sub-agents, todo tracking — into a lightweight Python implementation with only **three runtime dependencies**: `rich`, `httpx`, `prompt_toolkit`.
|
|
53
|
+
|
|
54
|
+
Works with any **OpenAI-compatible API**.
|
|
55
|
+
|
|
56
|
+
## Demo
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
pip install python-agent-harness # or: pip install -e ".[dev]" from source
|
|
64
|
+
python-agent-harness config --init
|
|
65
|
+
python-agent-harness run
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Optional extras: `[mcp]` for MCP server integration, `[dev]` for development tools.
|
|
69
|
+
|
|
70
|
+
Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_key`, and `model`.
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- **FSM-driven execution** — explicit `WAIT` / `TOOL` / `TRET` / `SUPERVISE` / `DONE` / `ERRS` / `ABRT` states. Completion supervision nudges the model when it stops early; failed tool calls are sanitized. Transient API failures (`429` / `5xx`) retry with exponential backoff. Auth-expired codes (configurable via `AUTH_REFRESH_STATUS_CODES` in `config.py`; defaults to `[401, 502]`) trigger automatic API key re-read from config/env.
|
|
75
|
+
- **Context management** — CJK-aware token estimation, per-model context windows, automatic compaction at 70% usage.
|
|
76
|
+
- **Image & text attachments** — attach with `@path` (e.g. `@screenshot.png`, `@README.md`): images (PNG/JPEG/GIF/WebP, up to 20 MB) become multimodal `image_url` parts; text files are inlined. Web URLs supported for images.
|
|
77
|
+
- **Coding tools** — `Agent`, `TodoWrite`, `Glob`, `Grep`, `Read`, `Insert`, `Edit` (incl. unified diffs), `Write`, `Mkdir`, `Bash`, `Skill`, `Question`, `LSP`, `PlanExit`. Read-only tool rounds (`Read`, `Glob`, `Grep`, `Skill`, `LSP`) dispatch concurrently; async tools (`Bash`, `Agent`) can also run concurrently. Results always delivered in emitted order.
|
|
78
|
+
- **Plan / Build modes** — plan mode is read-only except for the per-session plan file.
|
|
79
|
+
- **Persistent sessions** — auto-saved after every response to `~/.local/share/python-agent-harness/sessions/`, with LLM-generated titles and `/restore --latest` / `/sessions`.
|
|
80
|
+
- **Focused TUI** — Rich-based with pinned status bar, Todos panel, inline diff rendering, and a `prompt_toolkit` editor with history and completion. `Esc+Enter` submits, `Ctrl-D` quits, `Ctrl-C` cancels.
|
|
81
|
+
- **MCP support** — optional via `[mcp]` extra. MCP tools become ordinary agent tools (`mcp__<server>__<tool>`). Supports `stdio`, `streamable-http`, and `sse` transports.
|
|
82
|
+
- **Slash commands** — built-in `/init`, `/review`, `/explain`, etc., plus custom commands from `prompts/commands/*.md`.
|
|
83
|
+
- **Custom agents** — switch the main agent's system prompt at runtime with `/agent`. Agent files live in `prompts/agents/*.md` and support `name:` and `exclude_tools:` frontmatter.
|
|
84
|
+
- **Embeddable runtime boundaries** — `headless --json` for one-shot CI/scripting; `serve` for hosting apps (resident process, bidirectional protocol: multi-turn memory, mid-run Q&A, protocol-level cancel).
|
|
85
|
+
|
|
86
|
+
## Usage
|
|
87
|
+
|
|
88
|
+
### TUI mode
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
python-agent-harness run [--project DIR]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Headless mode
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
python-agent-harness headless [prompt] [--project DIR] [--restore [SPEC]] [--model NAME] [--json]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Runs a single prompt without the TUI — for CI, scripting, and piping. The assistant's answer goes to stdout; tool/status events go to stderr. Interactive prompts are auto-answered (`confirm` → yes, `ask` → "Unanswered").
|
|
101
|
+
|
|
102
|
+
- Prompt is a positional argument; when omitted, read from stdin.
|
|
103
|
+
- `--restore` continues a saved session. Bare `--restore` uses the most recent; a SPEC may be a file path or title substring.
|
|
104
|
+
- `--model NAME` selects a profile from the `models` config section, or a raw model name on the configured endpoint.
|
|
105
|
+
- `--json` emits JSON lines on stdout: `start`, `delta`, `notify`, `log`, and a final `result` (with structured `errors`). Each line carries `protocol` (wire schema version) and `seq` (monotonic counter). Exit code 0 on success, 1 on error.
|
|
106
|
+
|
|
107
|
+
### Serve mode
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
python-agent-harness serve [--project DIR] [--answer-timeout SECONDS] [--max-rounds N] [--timeout SECONDS]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A persistent, bidirectional runtime for hosting applications. Keeps the `Controller`/`Session` resident so the host can submit multiple prompts, answer mid-run questions, and cancel runs via protocol messages. See [docs/serve-protocol.md](docs/serve-protocol.md) for the full protocol specification.
|
|
114
|
+
|
|
115
|
+
### Slash commands
|
|
116
|
+
|
|
117
|
+
| Command | Description |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `/plan` / `/build` | Switch between read-only plan mode and build mode |
|
|
120
|
+
| `/init` | Create or update `AGENTS.md` |
|
|
121
|
+
| `/review` | Review uncommitted changes, commits, branches, or pull requests |
|
|
122
|
+
| `/explain [project] [target]` | Explain code |
|
|
123
|
+
| `/compact` | Compact the conversation |
|
|
124
|
+
| `/summary` | Append a conversation summary |
|
|
125
|
+
| `/save` | Save the current session |
|
|
126
|
+
| `/sessions` | List saved sessions |
|
|
127
|
+
| `/restore [path\|title\|--latest\|latest]` | Restore a session (substring search) |
|
|
128
|
+
| `/clear` | Start a fresh conversation |
|
|
129
|
+
| `/model [name]` | Switch LLM profiles; `default` restores original |
|
|
130
|
+
| `/agent [name]` | Switch agent prompt; `default` restores built-in |
|
|
131
|
+
| `/help` | List available commands |
|
|
132
|
+
| `/exit` | Quit |
|
|
133
|
+
|
|
134
|
+
Custom commands from `prompts/commands/*.md` are also registered (TUI only).
|
|
135
|
+
|
|
136
|
+
### Custom agents
|
|
137
|
+
|
|
138
|
+
Agent files are markdown (`.md`) in `prompts/agents/`. Each becomes a switchable profile via `/agent`. Optional YAML frontmatter:
|
|
139
|
+
|
|
140
|
+
- `name:` — override the agent name (defaults to file stem)
|
|
141
|
+
- `exclude_tools:` — list of tool names to hide. Matches by exact name, glob (`mcp__git__*`), or `__`-delimited prefix (`mcp__git` hides `mcp__git__list_repos` but `Write` does NOT hide `TodoWrite`).
|
|
142
|
+
|
|
143
|
+
**Commands vs Agents:** Commands (`/review`) are one-shot with a hardcoded kickoff and reset after run. Agents (`/agent reviewer`) persist until the next `/agent`, let the user type their own prompt, and stay active after `Ctrl-C`.
|
|
144
|
+
|
|
145
|
+
## Configuration
|
|
146
|
+
|
|
147
|
+
All LLM settings live in `~/.config/python-agent-harness/config.json`. Environment variables are optional.
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"llm": {
|
|
152
|
+
"base_url": "https://api.openai.com/v1",
|
|
153
|
+
"api_key": "sk-...",
|
|
154
|
+
"model": "gpt-5-mini",
|
|
155
|
+
"reasoning_effort": null,
|
|
156
|
+
"stream": true,
|
|
157
|
+
"supports_image_input": false
|
|
158
|
+
},
|
|
159
|
+
"models": {
|
|
160
|
+
"deepseek": { "base_url": "https://api.deepseek.com/v1", "model": "deepseek-chat" },
|
|
161
|
+
"qwen": { "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "model": "qwen3.5-coder" }
|
|
162
|
+
},
|
|
163
|
+
"context_windows": {
|
|
164
|
+
"deepseek-v4": 1000000
|
|
165
|
+
},
|
|
166
|
+
"paths": {
|
|
167
|
+
"context_path": null,
|
|
168
|
+
"skill_path": null
|
|
169
|
+
},
|
|
170
|
+
"mcp": {
|
|
171
|
+
"servers": {
|
|
172
|
+
"example": {
|
|
173
|
+
"transport": "stdio",
|
|
174
|
+
"command": "npx",
|
|
175
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
|
|
176
|
+
"env": [],
|
|
177
|
+
"parallel": false,
|
|
178
|
+
"timeout": null,
|
|
179
|
+
"enabled": false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Options
|
|
187
|
+
|
|
188
|
+
- **`llm`** — main LLM config. Optional keys: `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, `stream`, `supports_image_input`. `run --no-stream` overrides `stream`. `supports_image_input` (default `false`) controls whether `@path` images are sent or stripped.
|
|
189
|
+
- **`models`** — named LLM profiles for `/model` switching. Partial settings; unset keys inherit from `llm`. `default` restores the main config.
|
|
190
|
+
- **`context_windows`** — per-model context-window overrides (tokens). Keys are model names or substrings; first match wins. Overrides `CONTEXT_WINDOWS` in `config.py`.
|
|
191
|
+
- **`subagent_llm`** — LLM config for `Agent` tool. Unset values inherit from `llm`. Set `profile` to reuse a `models` entry. Precedence: profile > `subagent_llm` > `llm` > env vars.
|
|
192
|
+
- **`default_agent`** — agent name to use at session start (must exist in `prompts/agents/`). `null` = built-in default.
|
|
193
|
+
- **`paths.context_path` / `paths.skill_path`** — locations for context files and skills. Default: project-local `contexts/` and `skills/`.
|
|
194
|
+
- **`lsp.servers`** — per-extension LSP server overrides for the `LSP` tool. Keys are extensions (`.py`, `.cpp`); each has `command` (argv) and optional `language_id`. Layers on top of `DEFAULT_SERVERS` in `lsp/manager.py`.
|
|
195
|
+
- **`mcp.servers`** — MCP server config (requires `[mcp]` extra). Each supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, `enabled`.
|
|
196
|
+
- **`headless` / `serve`** — optional budget limits: `max_rounds` (LLM rounds) and `timeout` (wall-clock seconds). `--max-rounds` / `--timeout` override per call; `<= 0` or `null` disables. Unset = unlimited.
|
|
197
|
+
- **Precedence** — code defaults < config file < `OPENAI_*` env vars. Sub-agents also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
|
|
198
|
+
- **Custom config** — `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG` env var.
|
|
199
|
+
- **LLM logging** — request/response bodies logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change. Path printed at startup.
|
|
200
|
+
|
|
201
|
+
## Inspired by opencode
|
|
202
|
+
|
|
203
|
+
Most of [opencode](https://github.com/anomalyco/opencode)'s prompts and core behaviors have been ported. The goal: retain its practical coding-agent workflow while keeping the implementation small and easy to customize.
|
|
204
|
+
|
|
205
|
+
| opencode | python-agent-harness |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `default.txt` (main agent) | `agent.md` |
|
|
208
|
+
| `plan.txt` / `plan-mode.txt` / `build-switch.txt` | `plan.md` / `plan-mode.md` / `build-switch.md` |
|
|
209
|
+
| `task.txt` (sub-agent) | `subagent.md` + `Agent` tool |
|
|
210
|
+
| `todowrite.txt` / `question.txt` / `skill.txt` | `TodoWrite` / `Question` / `Skill` tools |
|
|
211
|
+
| `read.txt` / `write.txt` / `edit.txt` / `grep.txt` / `glob.txt` | `Read` / `Write` / `Edit` / `Grep` / `Glob` tools |
|
|
212
|
+
| `shell.txt` | `Bash` tool + `agent.md` Git/GitHub guidance |
|
|
213
|
+
| `plan-enter.txt` / `plan-exit.txt` | `PlanExit` tool |
|
|
214
|
+
| `initialize.txt` / `review.txt` / `explain` | `initialize.md` / `review.md` / `commands/explain.md` |
|
|
215
|
+
| compaction / summary / title | `compact.md` / `summary.md` / `title.md` |
|
|
216
|
+
| `AGENTS.md` handling | `prompts.py` (`find_agents_md_files`, `load_context_files`) |
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
Requires Python ≥ 3.11. CI runs against 3.11, 3.12, and 3.13 on Linux and macOS.
|
|
221
|
+
|
|
222
|
+
```sh
|
|
223
|
+
make test # unit tests
|
|
224
|
+
venv/bin/pip install -e ".[dev]" # development tools
|
|
225
|
+
venv/bin/ruff check . # lint
|
|
226
|
+
venv/bin/ruff format --check . # formatting (CI blocks on this)
|
|
227
|
+
venv/bin/pyright # type checking
|
|
228
|
+
venv/bin/python -m build # build sdist + wheel
|
|
229
|
+
venv/bin/pip-audit # dependency audit
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Related projects
|
|
233
|
+
|
|
234
|
+
- [gptel-agent-harness](https://github.com/beacoder/gptel-agent-harness) — the Emacs-based implementation that inspired this project.
|
|
235
|
+
- [opencode](https://github.com/anomalyco/opencode) — the primary source of many prompts and coding-agent behaviors.
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# python-agent-harness
|
|
4
|
+
|
|
5
|
+
**A lightweight, hackable mini-OpenCode written in Python.**
|
|
6
|
+
FSM-driven execution · OpenAI-compatible · built for daily use and easy customization
|
|
7
|
+
|
|
8
|
+
[](https://github.com/beacoder/python-agent-harness/actions/workflows/ci.yml)
|
|
9
|
+
[](https://pypi.org/project/python-agent-harness/)
|
|
10
|
+
[](https://www.python.org/downloads/)
|
|
11
|
+
[](https://pepy.tech/projects/python-agent-harness)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
A terminal coding agent that reads your codebase, plans changes, edits files, runs commands, and verifies its work.
|
|
17
|
+
|
|
18
|
+
Inspired by [gptel-agent-harness](https://github.com/beacoder/gptel-agent-harness) and [opencode](https://github.com/anomalyco/opencode). It brings opencode's prompts and core behaviors — `AGENTS.md` discovery, plan/build modes, skills, sub-agents, todo tracking — into a lightweight Python implementation with only **three runtime dependencies**: `rich`, `httpx`, `prompt_toolkit`.
|
|
19
|
+
|
|
20
|
+
Works with any **OpenAI-compatible API**.
|
|
21
|
+
|
|
22
|
+
## Demo
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pip install python-agent-harness # or: pip install -e ".[dev]" from source
|
|
30
|
+
python-agent-harness config --init
|
|
31
|
+
python-agent-harness run
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Optional extras: `[mcp]` for MCP server integration, `[dev]` for development tools.
|
|
35
|
+
|
|
36
|
+
Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_key`, and `model`.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **FSM-driven execution** — explicit `WAIT` / `TOOL` / `TRET` / `SUPERVISE` / `DONE` / `ERRS` / `ABRT` states. Completion supervision nudges the model when it stops early; failed tool calls are sanitized. Transient API failures (`429` / `5xx`) retry with exponential backoff. Auth-expired codes (configurable via `AUTH_REFRESH_STATUS_CODES` in `config.py`; defaults to `[401, 502]`) trigger automatic API key re-read from config/env.
|
|
41
|
+
- **Context management** — CJK-aware token estimation, per-model context windows, automatic compaction at 70% usage.
|
|
42
|
+
- **Image & text attachments** — attach with `@path` (e.g. `@screenshot.png`, `@README.md`): images (PNG/JPEG/GIF/WebP, up to 20 MB) become multimodal `image_url` parts; text files are inlined. Web URLs supported for images.
|
|
43
|
+
- **Coding tools** — `Agent`, `TodoWrite`, `Glob`, `Grep`, `Read`, `Insert`, `Edit` (incl. unified diffs), `Write`, `Mkdir`, `Bash`, `Skill`, `Question`, `LSP`, `PlanExit`. Read-only tool rounds (`Read`, `Glob`, `Grep`, `Skill`, `LSP`) dispatch concurrently; async tools (`Bash`, `Agent`) can also run concurrently. Results always delivered in emitted order.
|
|
44
|
+
- **Plan / Build modes** — plan mode is read-only except for the per-session plan file.
|
|
45
|
+
- **Persistent sessions** — auto-saved after every response to `~/.local/share/python-agent-harness/sessions/`, with LLM-generated titles and `/restore --latest` / `/sessions`.
|
|
46
|
+
- **Focused TUI** — Rich-based with pinned status bar, Todos panel, inline diff rendering, and a `prompt_toolkit` editor with history and completion. `Esc+Enter` submits, `Ctrl-D` quits, `Ctrl-C` cancels.
|
|
47
|
+
- **MCP support** — optional via `[mcp]` extra. MCP tools become ordinary agent tools (`mcp__<server>__<tool>`). Supports `stdio`, `streamable-http`, and `sse` transports.
|
|
48
|
+
- **Slash commands** — built-in `/init`, `/review`, `/explain`, etc., plus custom commands from `prompts/commands/*.md`.
|
|
49
|
+
- **Custom agents** — switch the main agent's system prompt at runtime with `/agent`. Agent files live in `prompts/agents/*.md` and support `name:` and `exclude_tools:` frontmatter.
|
|
50
|
+
- **Embeddable runtime boundaries** — `headless --json` for one-shot CI/scripting; `serve` for hosting apps (resident process, bidirectional protocol: multi-turn memory, mid-run Q&A, protocol-level cancel).
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### TUI mode
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
python-agent-harness run [--project DIR]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Headless mode
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
python-agent-harness headless [prompt] [--project DIR] [--restore [SPEC]] [--model NAME] [--json]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Runs a single prompt without the TUI — for CI, scripting, and piping. The assistant's answer goes to stdout; tool/status events go to stderr. Interactive prompts are auto-answered (`confirm` → yes, `ask` → "Unanswered").
|
|
67
|
+
|
|
68
|
+
- Prompt is a positional argument; when omitted, read from stdin.
|
|
69
|
+
- `--restore` continues a saved session. Bare `--restore` uses the most recent; a SPEC may be a file path or title substring.
|
|
70
|
+
- `--model NAME` selects a profile from the `models` config section, or a raw model name on the configured endpoint.
|
|
71
|
+
- `--json` emits JSON lines on stdout: `start`, `delta`, `notify`, `log`, and a final `result` (with structured `errors`). Each line carries `protocol` (wire schema version) and `seq` (monotonic counter). Exit code 0 on success, 1 on error.
|
|
72
|
+
|
|
73
|
+
### Serve mode
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
python-agent-harness serve [--project DIR] [--answer-timeout SECONDS] [--max-rounds N] [--timeout SECONDS]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A persistent, bidirectional runtime for hosting applications. Keeps the `Controller`/`Session` resident so the host can submit multiple prompts, answer mid-run questions, and cancel runs via protocol messages. See [docs/serve-protocol.md](docs/serve-protocol.md) for the full protocol specification.
|
|
80
|
+
|
|
81
|
+
### Slash commands
|
|
82
|
+
|
|
83
|
+
| Command | Description |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `/plan` / `/build` | Switch between read-only plan mode and build mode |
|
|
86
|
+
| `/init` | Create or update `AGENTS.md` |
|
|
87
|
+
| `/review` | Review uncommitted changes, commits, branches, or pull requests |
|
|
88
|
+
| `/explain [project] [target]` | Explain code |
|
|
89
|
+
| `/compact` | Compact the conversation |
|
|
90
|
+
| `/summary` | Append a conversation summary |
|
|
91
|
+
| `/save` | Save the current session |
|
|
92
|
+
| `/sessions` | List saved sessions |
|
|
93
|
+
| `/restore [path\|title\|--latest\|latest]` | Restore a session (substring search) |
|
|
94
|
+
| `/clear` | Start a fresh conversation |
|
|
95
|
+
| `/model [name]` | Switch LLM profiles; `default` restores original |
|
|
96
|
+
| `/agent [name]` | Switch agent prompt; `default` restores built-in |
|
|
97
|
+
| `/help` | List available commands |
|
|
98
|
+
| `/exit` | Quit |
|
|
99
|
+
|
|
100
|
+
Custom commands from `prompts/commands/*.md` are also registered (TUI only).
|
|
101
|
+
|
|
102
|
+
### Custom agents
|
|
103
|
+
|
|
104
|
+
Agent files are markdown (`.md`) in `prompts/agents/`. Each becomes a switchable profile via `/agent`. Optional YAML frontmatter:
|
|
105
|
+
|
|
106
|
+
- `name:` — override the agent name (defaults to file stem)
|
|
107
|
+
- `exclude_tools:` — list of tool names to hide. Matches by exact name, glob (`mcp__git__*`), or `__`-delimited prefix (`mcp__git` hides `mcp__git__list_repos` but `Write` does NOT hide `TodoWrite`).
|
|
108
|
+
|
|
109
|
+
**Commands vs Agents:** Commands (`/review`) are one-shot with a hardcoded kickoff and reset after run. Agents (`/agent reviewer`) persist until the next `/agent`, let the user type their own prompt, and stay active after `Ctrl-C`.
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
|
|
113
|
+
All LLM settings live in `~/.config/python-agent-harness/config.json`. Environment variables are optional.
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"llm": {
|
|
118
|
+
"base_url": "https://api.openai.com/v1",
|
|
119
|
+
"api_key": "sk-...",
|
|
120
|
+
"model": "gpt-5-mini",
|
|
121
|
+
"reasoning_effort": null,
|
|
122
|
+
"stream": true,
|
|
123
|
+
"supports_image_input": false
|
|
124
|
+
},
|
|
125
|
+
"models": {
|
|
126
|
+
"deepseek": { "base_url": "https://api.deepseek.com/v1", "model": "deepseek-chat" },
|
|
127
|
+
"qwen": { "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "model": "qwen3.5-coder" }
|
|
128
|
+
},
|
|
129
|
+
"context_windows": {
|
|
130
|
+
"deepseek-v4": 1000000
|
|
131
|
+
},
|
|
132
|
+
"paths": {
|
|
133
|
+
"context_path": null,
|
|
134
|
+
"skill_path": null
|
|
135
|
+
},
|
|
136
|
+
"mcp": {
|
|
137
|
+
"servers": {
|
|
138
|
+
"example": {
|
|
139
|
+
"transport": "stdio",
|
|
140
|
+
"command": "npx",
|
|
141
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
|
|
142
|
+
"env": [],
|
|
143
|
+
"parallel": false,
|
|
144
|
+
"timeout": null,
|
|
145
|
+
"enabled": false
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Options
|
|
153
|
+
|
|
154
|
+
- **`llm`** — main LLM config. Optional keys: `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, `stream`, `supports_image_input`. `run --no-stream` overrides `stream`. `supports_image_input` (default `false`) controls whether `@path` images are sent or stripped.
|
|
155
|
+
- **`models`** — named LLM profiles for `/model` switching. Partial settings; unset keys inherit from `llm`. `default` restores the main config.
|
|
156
|
+
- **`context_windows`** — per-model context-window overrides (tokens). Keys are model names or substrings; first match wins. Overrides `CONTEXT_WINDOWS` in `config.py`.
|
|
157
|
+
- **`subagent_llm`** — LLM config for `Agent` tool. Unset values inherit from `llm`. Set `profile` to reuse a `models` entry. Precedence: profile > `subagent_llm` > `llm` > env vars.
|
|
158
|
+
- **`default_agent`** — agent name to use at session start (must exist in `prompts/agents/`). `null` = built-in default.
|
|
159
|
+
- **`paths.context_path` / `paths.skill_path`** — locations for context files and skills. Default: project-local `contexts/` and `skills/`.
|
|
160
|
+
- **`lsp.servers`** — per-extension LSP server overrides for the `LSP` tool. Keys are extensions (`.py`, `.cpp`); each has `command` (argv) and optional `language_id`. Layers on top of `DEFAULT_SERVERS` in `lsp/manager.py`.
|
|
161
|
+
- **`mcp.servers`** — MCP server config (requires `[mcp]` extra). Each supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, `enabled`.
|
|
162
|
+
- **`headless` / `serve`** — optional budget limits: `max_rounds` (LLM rounds) and `timeout` (wall-clock seconds). `--max-rounds` / `--timeout` override per call; `<= 0` or `null` disables. Unset = unlimited.
|
|
163
|
+
- **Precedence** — code defaults < config file < `OPENAI_*` env vars. Sub-agents also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
|
|
164
|
+
- **Custom config** — `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG` env var.
|
|
165
|
+
- **LLM logging** — request/response bodies logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change. Path printed at startup.
|
|
166
|
+
|
|
167
|
+
## Inspired by opencode
|
|
168
|
+
|
|
169
|
+
Most of [opencode](https://github.com/anomalyco/opencode)'s prompts and core behaviors have been ported. The goal: retain its practical coding-agent workflow while keeping the implementation small and easy to customize.
|
|
170
|
+
|
|
171
|
+
| opencode | python-agent-harness |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `default.txt` (main agent) | `agent.md` |
|
|
174
|
+
| `plan.txt` / `plan-mode.txt` / `build-switch.txt` | `plan.md` / `plan-mode.md` / `build-switch.md` |
|
|
175
|
+
| `task.txt` (sub-agent) | `subagent.md` + `Agent` tool |
|
|
176
|
+
| `todowrite.txt` / `question.txt` / `skill.txt` | `TodoWrite` / `Question` / `Skill` tools |
|
|
177
|
+
| `read.txt` / `write.txt` / `edit.txt` / `grep.txt` / `glob.txt` | `Read` / `Write` / `Edit` / `Grep` / `Glob` tools |
|
|
178
|
+
| `shell.txt` | `Bash` tool + `agent.md` Git/GitHub guidance |
|
|
179
|
+
| `plan-enter.txt` / `plan-exit.txt` | `PlanExit` tool |
|
|
180
|
+
| `initialize.txt` / `review.txt` / `explain` | `initialize.md` / `review.md` / `commands/explain.md` |
|
|
181
|
+
| compaction / summary / title | `compact.md` / `summary.md` / `title.md` |
|
|
182
|
+
| `AGENTS.md` handling | `prompts.py` (`find_agents_md_files`, `load_context_files`) |
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
Requires Python ≥ 3.11. CI runs against 3.11, 3.12, and 3.13 on Linux and macOS.
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
make test # unit tests
|
|
190
|
+
venv/bin/pip install -e ".[dev]" # development tools
|
|
191
|
+
venv/bin/ruff check . # lint
|
|
192
|
+
venv/bin/ruff format --check . # formatting (CI blocks on this)
|
|
193
|
+
venv/bin/pyright # type checking
|
|
194
|
+
venv/bin/python -m build # build sdist + wheel
|
|
195
|
+
venv/bin/pip-audit # dependency audit
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Related projects
|
|
199
|
+
|
|
200
|
+
- [gptel-agent-harness](https://github.com/beacoder/gptel-agent-harness) — the Emacs-based implementation that inspired this project.
|
|
201
|
+
- [opencode](https://github.com/anomalyco/opencode) — the primary source of many prompts and coding-agent behaviors.
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-agent-harness"
|
|
7
|
-
version = "1.5.6.
|
|
7
|
+
version = "1.5.6.2"
|
|
8
8
|
description = "A lightweight, hackable mini-OpenCode written in Python."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Huming Chen", email = "chenhuming@gmail.com"}
|
{python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/core/agent.py
RENAMED
|
@@ -43,7 +43,7 @@ from ..llm.client import LLMClient
|
|
|
43
43
|
from ..session import config
|
|
44
44
|
from ..tools.base import PendingToolResult
|
|
45
45
|
from .context_manager import ContextManager
|
|
46
|
-
from .models import Message, ToolCall
|
|
46
|
+
from .models import Message, ToolCall, display_args
|
|
47
47
|
from .token_estimator import context_window_for, estimate_payload_tokens
|
|
48
48
|
from .tool_runner import (
|
|
49
49
|
NIL_RESULT_PLACEHOLDER, # noqa: F401 (re-exported for backward compat)
|
|
@@ -137,6 +137,20 @@ class AgentLoop:
|
|
|
137
137
|
self._local = threading.local()
|
|
138
138
|
self.terminal_text: str | None = None
|
|
139
139
|
self.result: str | None = None
|
|
140
|
+
# True right after an auto-compaction: the next WAIT must send
|
|
141
|
+
# the request instead of compacting again. Compaction keeps
|
|
142
|
+
# every user prompt verbatim, so when the irreducible part (the
|
|
143
|
+
# prompts, the system prompt, the tool specs) alone exceeds
|
|
144
|
+
# CONTEXT_TRIGGER, re-checking the ratio would compact forever
|
|
145
|
+
# -- one paid summary request per iteration.
|
|
146
|
+
self._just_compacted = False
|
|
147
|
+
# The context ratio measured right after a compaction that could
|
|
148
|
+
# NOT bring it below the trigger (None otherwise). While set,
|
|
149
|
+
# compacting again only pays off once the conversation has grown
|
|
150
|
+
# well past that floor; without it every tool round would be
|
|
151
|
+
# compacted again -- doubling the requests and summarizing each
|
|
152
|
+
# fresh tool result away before the model ever saw it.
|
|
153
|
+
self._compaction_floor: float | None = None
|
|
140
154
|
# Cancellation identity for this run: cancel() bumps the session
|
|
141
155
|
# generation, so a stale worker from a cancelled run stays
|
|
142
156
|
# cancelled even after the next run clears the shared event (and
|
|
@@ -150,6 +164,16 @@ class AgentLoop:
|
|
|
150
164
|
# from cancellation: a cancelled run with no successor still
|
|
151
165
|
# owns the session and may salvage its partial history.
|
|
152
166
|
self._run_gen = session.run_generation
|
|
167
|
+
# Tag THIS thread (the one that will drive the loop — the run
|
|
168
|
+
# worker, or a sub-agent's own thread) with the run it belongs
|
|
169
|
+
# to, so the session can drop events from a superseded run
|
|
170
|
+
# instead of attributing them to the current one. A detached
|
|
171
|
+
# sub-agent thread can outlive its run; see
|
|
172
|
+
# `Session._emit_superseded`. Suppressed for session doubles
|
|
173
|
+
# that predate the hook.
|
|
174
|
+
mark = getattr(session, "mark_emit_generation", None)
|
|
175
|
+
if callable(mark):
|
|
176
|
+
mark(self._run_gen)
|
|
153
177
|
|
|
154
178
|
def _is_cancelled(self) -> bool:
|
|
155
179
|
"""Whether THIS run must stop (cancelled or superseded).
|
|
@@ -205,14 +229,19 @@ class AgentLoop:
|
|
|
205
229
|
if self.top_level:
|
|
206
230
|
self.error = "Error: round budget exhausted before the run finished"
|
|
207
231
|
self.info["error"] = self.error
|
|
208
|
-
|
|
232
|
+
# The code is assigned HERE, where the outcome is known.
|
|
233
|
+
# A driver reading the result line gets "budget" because
|
|
234
|
+
# the budget check said so — not because a downstream
|
|
235
|
+
# classifier matched words in this sentence, which would
|
|
236
|
+
# silently degrade to "unknown" if it were reworded.
|
|
237
|
+
self.session.notify("error", {"code": "budget", "message": self.error})
|
|
209
238
|
else:
|
|
210
239
|
self.info["budget"] = True
|
|
211
240
|
return True
|
|
212
241
|
if self._deadline_exceeded():
|
|
213
242
|
self.error = f"Error: {_deadline_message(self.timeout)}"
|
|
214
243
|
self.info["error"] = self.error
|
|
215
|
-
self.session.notify("error", self.error)
|
|
244
|
+
self.session.notify("error", {"code": "timeout", "message": self.error})
|
|
216
245
|
return True
|
|
217
246
|
return False
|
|
218
247
|
|
|
@@ -497,14 +526,43 @@ class AgentLoop:
|
|
|
497
526
|
# so their ratio/usage would skew the parent's
|
|
498
527
|
self._update_context_ratio()
|
|
499
528
|
if self._need_compaction():
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
529
|
+
ratio = session.context_ratio
|
|
530
|
+
floor = self._compaction_floor
|
|
531
|
+
if self._just_compacted:
|
|
532
|
+
# Still above the trigger right after compacting:
|
|
533
|
+
# what is left is (mostly) irreducible. Send the
|
|
534
|
+
# request -- it either fits, or fails with a
|
|
535
|
+
# context-length error (ERRS) instead of looping --
|
|
536
|
+
# and remember the floor so later rounds do not
|
|
537
|
+
# compact again for nothing.
|
|
538
|
+
self._compaction_floor = ratio
|
|
539
|
+
session.log(
|
|
540
|
+
f"context {ratio:.1%} still above the compaction "
|
|
541
|
+
"trigger after compacting; sending anyway"
|
|
542
|
+
)
|
|
543
|
+
elif floor is not None and ratio <= floor + max(0.0, 1.0 - floor) / 2:
|
|
544
|
+
# Not yet grown by half the remaining headroom over
|
|
545
|
+
# the post-compaction floor (any growth at all once
|
|
546
|
+
# the floor is past the window): compacting now
|
|
547
|
+
# could only reclaim the latest rounds' output.
|
|
548
|
+
pass
|
|
549
|
+
else:
|
|
550
|
+
session.log(f"compacting context {ratio:.1%}")
|
|
551
|
+
if self.compact():
|
|
552
|
+
self._just_compacted = True
|
|
553
|
+
self.info["compacted"] = True
|
|
554
|
+
return
|
|
555
|
+
else:
|
|
556
|
+
self._compaction_floor = None
|
|
557
|
+
self._just_compacted = False
|
|
504
558
|
|
|
505
559
|
def safe_delta(text: str) -> None:
|
|
506
|
-
|
|
507
|
-
|
|
560
|
+
# Snapshot: a host that swaps views per run unwires
|
|
561
|
+
# on_delta at run teardown, so re-reading it after the
|
|
562
|
+
# None-test could call None (see Session.notify).
|
|
563
|
+
on_delta = session.on_delta
|
|
564
|
+
if not self._is_cancelled() and on_delta is not None:
|
|
565
|
+
on_delta(text)
|
|
508
566
|
|
|
509
567
|
try:
|
|
510
568
|
# sub-agents are one-shot tasks: they must not see (or
|
|
@@ -581,6 +639,20 @@ class AgentLoop:
|
|
|
581
639
|
totals["input"] = totals.get("input", 0) + usage.input_tokens
|
|
582
640
|
totals["output"] = totals.get("output", 0) + usage.output_tokens
|
|
583
641
|
totals["rounds"] = totals.get("rounds", 0) + 1
|
|
642
|
+
snapshot = {
|
|
643
|
+
"input": totals["input"],
|
|
644
|
+
"output": totals["output"],
|
|
645
|
+
"rounds": totals["rounds"],
|
|
646
|
+
}
|
|
647
|
+
# Publish the running total so a host can meter DURING the
|
|
648
|
+
# run instead of only billing the terminal result line.
|
|
649
|
+
# Without this a run that outlives its user's allowance
|
|
650
|
+
# cannot be stopped until it finishes on its own — the host
|
|
651
|
+
# can now cancel mid-run. Emitted from the top-level loop
|
|
652
|
+
# only (the snapshot already includes sub-agent tokens, so
|
|
653
|
+
# per-sub-agent lines would add noise, not information).
|
|
654
|
+
if self.top_level:
|
|
655
|
+
session.notify("usage", snapshot)
|
|
584
656
|
|
|
585
657
|
# persist the assistant response in the conversation history
|
|
586
658
|
# (text and/or tool calls) so later turns and the UI see it;
|
|
@@ -621,6 +693,15 @@ class AgentLoop:
|
|
|
621
693
|
self.session.last_messages = list(self.messages)
|
|
622
694
|
names = [tc.name for tc in self.pending]
|
|
623
695
|
self.session.notify("tool_start", names)
|
|
696
|
+
# Additive companion event: the same round, but with each
|
|
697
|
+
# call's arguments so a hosting UI can show WHAT is about to
|
|
698
|
+
# run ("Bash(command='ls -la')"), which bare names cannot
|
|
699
|
+
# convey. Kept separate from tool_start so the payload shape
|
|
700
|
+
# of the documented protocol stays backward compatible.
|
|
701
|
+
self.session.notify(
|
|
702
|
+
"tool_calls",
|
|
703
|
+
[{"name": tc.name, "args": display_args(tc.arguments)} for tc in self.pending],
|
|
704
|
+
)
|
|
624
705
|
self._execute_pending()
|
|
625
706
|
|
|
626
707
|
def _handle_tret(self) -> None:
|
{python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.2}/python_agent_harness/core/models.py
RENAMED
|
@@ -80,6 +80,39 @@ class ToolCall:
|
|
|
80
80
|
elapsed: float | None = None # execution wall-time in seconds (TUI display)
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def arg_repr(value: Any, limit: int = 100) -> str:
|
|
84
|
+
"""repr() of a tool-call argument, truncated to LIMIT chars total.
|
|
85
|
+
|
|
86
|
+
Long values (a long Bash command, a big JSON payload) are cut with
|
|
87
|
+
an ellipsis instead of being dropped from the label entirely.
|
|
88
|
+
"""
|
|
89
|
+
r = repr(value)
|
|
90
|
+
if len(r) <= limit:
|
|
91
|
+
return r
|
|
92
|
+
return r[: limit - 1] + "…"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def display_args(arguments: dict[str, Any] | str, limit: int = 100) -> dict[str, str]:
|
|
96
|
+
"""Tool-call arguments as short display strings, keyed by name.
|
|
97
|
+
|
|
98
|
+
``arguments`` arrives either already decoded or as the raw JSON
|
|
99
|
+
string the model emitted (unparseable when the model truncated it).
|
|
100
|
+
``content`` is skipped: a Write/Edit payload is far too large for a
|
|
101
|
+
one-line label, and it is shown as a diff instead.
|
|
102
|
+
|
|
103
|
+
Values are truncated, so the result is a display artifact -- never
|
|
104
|
+
the data a tool is executed with.
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(arguments, str):
|
|
107
|
+
try:
|
|
108
|
+
arguments = json.loads(arguments)
|
|
109
|
+
except (json.JSONDecodeError, ValueError):
|
|
110
|
+
return {}
|
|
111
|
+
if not isinstance(arguments, dict):
|
|
112
|
+
return {}
|
|
113
|
+
return {k: arg_repr(v, limit) for k, v in arguments.items() if k != "content"}
|
|
114
|
+
|
|
115
|
+
|
|
83
116
|
@dataclass
|
|
84
117
|
class Message:
|
|
85
118
|
"""One conversation message in OpenAI-compatible format.
|