python-agent-harness 1.5.5.9__tar.gz → 1.5.6.0__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.9/python_agent_harness.egg-info → python_agent_harness-1.5.6.0}/PKG-INFO +46 -3
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/README.md +44 -1
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/pyproject.toml +2 -6
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/core/agent.py +88 -4
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/entry/cli.py +93 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/entry/controller.py +22 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/entry/headless.py +198 -40
- python_agent_harness-1.5.6.0/python_agent_harness/entry/server.py +534 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/llm/client.py +2 -12
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/agent.md +1 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/subagent.md +1 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/session/config.py +40 -14
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/session/session.py +7 -1
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/__init__.py +4 -16
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/base.py +1 -1
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/bash.py +1 -3
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/diffapply.py +8 -3
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/edit.py +3 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/filesystem.py +97 -6
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/insert.py +3 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/lsp.py +0 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/write.py +8 -6
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tui/commands.py +1 -1
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tui/input.py +11 -16
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0/python_agent_harness.egg-info}/PKG-INFO +46 -3
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness.egg-info/SOURCES.txt +1 -4
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness.egg-info/requires.txt +1 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/tests/test_invariants.py +1 -1
- python_agent_harness-1.5.5.9/python_agent_harness/tools/bash_win.py +0 -232
- python_agent_harness-1.5.5.9/python_agent_harness/tools/edit_win.py +0 -56
- python_agent_harness-1.5.5.9/python_agent_harness/tools/glob_win.py +0 -104
- python_agent_harness-1.5.5.9/python_agent_harness/tools/grep_win.py +0 -135
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/LICENSE +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/core/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/core/context_manager.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/core/models.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/core/planmode.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/core/subagent.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/core/token_estimator.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/core/tool_runner.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/entry/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/entry/view.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/io/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/io/attachments.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/io/diffrender.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/io/persistence.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/io/text_filter.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/llm/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/lsp/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/lsp/client.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/lsp/config.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/lsp/manager.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/agents/reviewer.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/core.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/session/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/session/commands.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/glob.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/glob_mac.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/grep.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/grep_mac.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/setup.cfg +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/tests/test_scenarios.py +0 -0
{python_agent_harness-1.5.5.9/python_agent_harness.egg-info → python_agent_harness-1.5.6.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.6.0
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
5
|
Author-email: Huming Chen <chenhuming@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,7 +9,6 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
9
9
|
Classifier: Environment :: Console
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Operating System :: MacOS
|
|
12
|
-
Classifier: Operating System :: Microsoft :: Windows
|
|
13
12
|
Classifier: Operating System :: POSIX :: Linux
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -20,6 +19,7 @@ Description-Content-Type: text/markdown
|
|
|
20
19
|
License-File: LICENSE
|
|
21
20
|
Requires-Dist: rich>=13.0
|
|
22
21
|
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: certifi>=2024.0
|
|
23
23
|
Requires-Dist: prompt_toolkit>=3.0
|
|
24
24
|
Provides-Extra: mcp
|
|
25
25
|
Requires-Dist: mcp<3,>=2.0; extra == "mcp"
|
|
@@ -113,6 +113,7 @@ Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_
|
|
|
113
113
|
- **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.
|
|
114
114
|
- **Slash commands** — built-in `/init`, `/review`, `/explain`, and other commands, plus custom commands loaded from `prompts/commands/*.md`.
|
|
115
115
|
- **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.
|
|
116
|
+
- **Embeddable runtime boundaries** — drive the agent from programs: `headless --json` for one-shot CI/scripting (write-only JSONL stream), `serve` for hosting apps (resident process, bidirectional protocol: multi-turn memory, mid-run Q&A, protocol-level cancel).
|
|
116
117
|
|
|
117
118
|
## Inspired by opencode
|
|
118
119
|
|
|
@@ -281,6 +282,48 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
281
282
|
- Exit code is 0 on success, 1 when the prompt was empty (only failed
|
|
282
283
|
`@file` references), the run raised an agent error, or the restore failed.
|
|
283
284
|
|
|
285
|
+
### Serve mode (resident JSONL server)
|
|
286
|
+
|
|
287
|
+
```sh
|
|
288
|
+
python-agent-harness serve [--project DIR] [--answer-timeout SECONDS]
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
A persistent, bidirectional runtime boundary for hosting applications
|
|
292
|
+
(a web backend, an IDE, a CI driver). Unlike `headless --json` (one
|
|
293
|
+
prompt per process, write-only stream), `serve` keeps the
|
|
294
|
+
`Controller`/`Session` resident and speaks a request/response protocol
|
|
295
|
+
over stdin/stdout — the same process boundary (containerizable), but
|
|
296
|
+
the host can:
|
|
297
|
+
|
|
298
|
+
- submit multiple prompts over the process's lifetime — no per-turn
|
|
299
|
+
interpreter spawn, and conversation history is retained between them
|
|
300
|
+
(multi-turn memory);
|
|
301
|
+
- answer the agent's mid-run questions (the `Question` tool and
|
|
302
|
+
plan-exit confirmation) via an `answer` op;
|
|
303
|
+
- cancel a run as a protocol message (no signal semantics).
|
|
304
|
+
|
|
305
|
+
Protocol (one JSON object per line):
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
host → agent: {"op": "submit", "prompt": ..., "run_id": ...}
|
|
309
|
+
{"op": "answer", "run_id": ..., "answers": [...]}
|
|
310
|
+
{"op": "cancel", "run_id": ...}
|
|
311
|
+
{"op": "ping"} | {"op": "shutdown"}
|
|
312
|
+
agent → host: {"type": "ready"} first line
|
|
313
|
+
{"seq": N, "type": "start"|"delta"|"notify"|"log", "run_id": ...}
|
|
314
|
+
{"seq": N, "type": "result", "run_id": ..., "answer": ...,
|
|
315
|
+
"errors": [...], "usage": {...}, "cancelled": bool}
|
|
316
|
+
{"type": "error", "error": ...} protocol failures
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
A mid-run question arrives as a `notify` with `kind: "ask"` (data has
|
|
320
|
+
`kind: "ask"|"confirm"`); reply with `answer`. One run at a time; a
|
|
321
|
+
`submit` while one is active is rejected with an `error` line.
|
|
322
|
+
`--answer-timeout SECONDS` bounds how long a pending question waits
|
|
323
|
+
for the host's answer (default: forever). The result line's shape is
|
|
324
|
+
identical to `headless --json`'s, so a driver can speak both
|
|
325
|
+
protocols with one parser.
|
|
326
|
+
|
|
284
327
|
### Slash commands
|
|
285
328
|
|
|
286
329
|
| Command | Description |
|
|
@@ -335,7 +378,7 @@ You are a personal assistant. You do NOT modify files or run shell commands.
|
|
|
335
378
|
|
|
336
379
|
## Development
|
|
337
380
|
|
|
338
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux
|
|
381
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and MacOS.
|
|
339
382
|
|
|
340
383
|
```sh
|
|
341
384
|
make test # unit tests
|
|
@@ -79,6 +79,7 @@ Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_
|
|
|
79
79
|
- **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.
|
|
80
80
|
- **Slash commands** — built-in `/init`, `/review`, `/explain`, and other commands, plus custom commands loaded from `prompts/commands/*.md`.
|
|
81
81
|
- **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.
|
|
82
|
+
- **Embeddable runtime boundaries** — drive the agent from programs: `headless --json` for one-shot CI/scripting (write-only JSONL stream), `serve` for hosting apps (resident process, bidirectional protocol: multi-turn memory, mid-run Q&A, protocol-level cancel).
|
|
82
83
|
|
|
83
84
|
## Inspired by opencode
|
|
84
85
|
|
|
@@ -247,6 +248,48 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
247
248
|
- Exit code is 0 on success, 1 when the prompt was empty (only failed
|
|
248
249
|
`@file` references), the run raised an agent error, or the restore failed.
|
|
249
250
|
|
|
251
|
+
### Serve mode (resident JSONL server)
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
python-agent-harness serve [--project DIR] [--answer-timeout SECONDS]
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
A persistent, bidirectional runtime boundary for hosting applications
|
|
258
|
+
(a web backend, an IDE, a CI driver). Unlike `headless --json` (one
|
|
259
|
+
prompt per process, write-only stream), `serve` keeps the
|
|
260
|
+
`Controller`/`Session` resident and speaks a request/response protocol
|
|
261
|
+
over stdin/stdout — the same process boundary (containerizable), but
|
|
262
|
+
the host can:
|
|
263
|
+
|
|
264
|
+
- submit multiple prompts over the process's lifetime — no per-turn
|
|
265
|
+
interpreter spawn, and conversation history is retained between them
|
|
266
|
+
(multi-turn memory);
|
|
267
|
+
- answer the agent's mid-run questions (the `Question` tool and
|
|
268
|
+
plan-exit confirmation) via an `answer` op;
|
|
269
|
+
- cancel a run as a protocol message (no signal semantics).
|
|
270
|
+
|
|
271
|
+
Protocol (one JSON object per line):
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
host → agent: {"op": "submit", "prompt": ..., "run_id": ...}
|
|
275
|
+
{"op": "answer", "run_id": ..., "answers": [...]}
|
|
276
|
+
{"op": "cancel", "run_id": ...}
|
|
277
|
+
{"op": "ping"} | {"op": "shutdown"}
|
|
278
|
+
agent → host: {"type": "ready"} first line
|
|
279
|
+
{"seq": N, "type": "start"|"delta"|"notify"|"log", "run_id": ...}
|
|
280
|
+
{"seq": N, "type": "result", "run_id": ..., "answer": ...,
|
|
281
|
+
"errors": [...], "usage": {...}, "cancelled": bool}
|
|
282
|
+
{"type": "error", "error": ...} protocol failures
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
A mid-run question arrives as a `notify` with `kind: "ask"` (data has
|
|
286
|
+
`kind: "ask"|"confirm"`); reply with `answer`. One run at a time; a
|
|
287
|
+
`submit` while one is active is rejected with an `error` line.
|
|
288
|
+
`--answer-timeout SECONDS` bounds how long a pending question waits
|
|
289
|
+
for the host's answer (default: forever). The result line's shape is
|
|
290
|
+
identical to `headless --json`'s, so a driver can speak both
|
|
291
|
+
protocols with one parser.
|
|
292
|
+
|
|
250
293
|
### Slash commands
|
|
251
294
|
|
|
252
295
|
| Command | Description |
|
|
@@ -301,7 +344,7 @@ You are a personal assistant. You do NOT modify files or run shell commands.
|
|
|
301
344
|
|
|
302
345
|
## Development
|
|
303
346
|
|
|
304
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux
|
|
347
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and MacOS.
|
|
305
348
|
|
|
306
349
|
```sh
|
|
307
350
|
make test # unit tests
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-agent-harness"
|
|
7
|
-
version = "1.5.
|
|
7
|
+
version = "1.5.6.0"
|
|
8
8
|
description = "A lightweight, hackable mini-OpenCode written in Python."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Huming Chen", email = "chenhuming@gmail.com"}
|
|
@@ -17,7 +17,6 @@ classifiers = [
|
|
|
17
17
|
"Environment :: Console",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"Operating System :: MacOS",
|
|
20
|
-
"Operating System :: Microsoft :: Windows",
|
|
21
20
|
"Operating System :: POSIX :: Linux",
|
|
22
21
|
"Programming Language :: Python :: 3.11",
|
|
23
22
|
"Programming Language :: Python :: 3.12",
|
|
@@ -27,6 +26,7 @@ classifiers = [
|
|
|
27
26
|
dependencies = [
|
|
28
27
|
"rich>=13.0",
|
|
29
28
|
"httpx>=0.27",
|
|
29
|
+
"certifi>=2024.0",
|
|
30
30
|
"prompt_toolkit>=3.0",
|
|
31
31
|
]
|
|
32
32
|
|
|
@@ -96,8 +96,4 @@ omit = [
|
|
|
96
96
|
|
|
97
97
|
[tool.coverage.report]
|
|
98
98
|
show_missing = true
|
|
99
|
-
# Linux/macOS gate: 95%. Windows CI enforces 94% with tools/bash.py
|
|
100
|
-
# omitted instead (see .github/workflows/ci.yml): that module is the
|
|
101
|
-
# posix PTY/spawn backend whose body cannot execute on Windows, where
|
|
102
|
-
# tools/bash_win.py runs in its place.
|
|
103
99
|
fail_under = 95
|
{python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/core/agent.py
RENAMED
|
@@ -35,6 +35,8 @@ to them via ``_tool_runner`` / ``_context_manager``.
|
|
|
35
35
|
|
|
36
36
|
from __future__ import annotations
|
|
37
37
|
|
|
38
|
+
import threading
|
|
39
|
+
import time
|
|
38
40
|
from typing import Any, Protocol
|
|
39
41
|
|
|
40
42
|
from ..llm.client import LLMClient
|
|
@@ -50,6 +52,13 @@ from .tool_runner import (
|
|
|
50
52
|
)
|
|
51
53
|
|
|
52
54
|
|
|
55
|
+
def _deadline_message(timeout: float | None) -> str:
|
|
56
|
+
"""Result text for a wall-clock abort."""
|
|
57
|
+
if timeout:
|
|
58
|
+
return f"run exceeded the {timeout:g}s wall-clock limit"
|
|
59
|
+
return "run exceeded its wall-clock limit"
|
|
60
|
+
|
|
61
|
+
|
|
53
62
|
class _SupervisorSession(Protocol):
|
|
54
63
|
"""The slice of an agent session the Supervisor reads."""
|
|
55
64
|
|
|
@@ -76,6 +85,8 @@ class AgentLoop:
|
|
|
76
85
|
system: str | None = None,
|
|
77
86
|
max_rounds: int = 60,
|
|
78
87
|
client: LLMClient | None = None,
|
|
88
|
+
budget_top_level: bool = False,
|
|
89
|
+
timeout: float | None = None,
|
|
79
90
|
) -> None:
|
|
80
91
|
self.session = session
|
|
81
92
|
self.messages: list[Message] = messages if messages is not None else []
|
|
@@ -96,6 +107,13 @@ class AgentLoop:
|
|
|
96
107
|
# max_rounds only bounds sub-agent loops: the main agent runs until
|
|
97
108
|
# the model gives a terminal response or the user aborts it (Ctrl-C)
|
|
98
109
|
self.max_rounds = max_rounds if not top_level else None
|
|
110
|
+
if top_level and budget_top_level:
|
|
111
|
+
# unattended (headless/CI) top-level runs may opt in to a
|
|
112
|
+
# round budget: an interactive TUI run still runs until the
|
|
113
|
+
# model finishes or the user aborts
|
|
114
|
+
self.max_rounds = max_rounds
|
|
115
|
+
# Optional wall-clock budget for unattended runs (None = off)
|
|
116
|
+
self.timeout = timeout
|
|
99
117
|
self.pending: list[ToolCall] = []
|
|
100
118
|
self.error: str | None = None
|
|
101
119
|
self.harness_injected: bool = False
|
|
@@ -114,6 +132,9 @@ class AgentLoop:
|
|
|
114
132
|
self.info: dict[str, Any] = {}
|
|
115
133
|
self.history: list[str] = []
|
|
116
134
|
self.rounds = 0
|
|
135
|
+
# Thread-local wall-clock deadline bookkeeping: sub-agent loops
|
|
136
|
+
# run in their own threads, so a shared attribute would race
|
|
137
|
+
self._local = threading.local()
|
|
117
138
|
self.terminal_text: str | None = None
|
|
118
139
|
self.result: str | None = None
|
|
119
140
|
# Cancellation identity for this run: cancel() bumps the session
|
|
@@ -132,7 +153,6 @@ class AgentLoop:
|
|
|
132
153
|
|
|
133
154
|
def _is_cancelled(self) -> bool:
|
|
134
155
|
"""Whether THIS run must stop (cancelled or superseded).
|
|
135
|
-
|
|
136
156
|
The plain event is not enough: `_start_agent` clears it before
|
|
137
157
|
every run, so a worker from a cancelled run that finishes late
|
|
138
158
|
(e.g. after a long tool call) would otherwise see it cleared and
|
|
@@ -156,6 +176,46 @@ class AgentLoop:
|
|
|
156
176
|
"""
|
|
157
177
|
return self.session.run_generation != self._run_gen
|
|
158
178
|
|
|
179
|
+
@property
|
|
180
|
+
def _deadline(self) -> float | None:
|
|
181
|
+
"""This thread's wall-clock deadline (None = unbounded).
|
|
182
|
+
|
|
183
|
+
Sub-agent loops run in their own threads; the thread-local keeps
|
|
184
|
+
the parent's deadline from leaking into (or being cleared by)
|
|
185
|
+
their bookkeeping, while sibling sub-agents under one session
|
|
186
|
+
each read their own entry.
|
|
187
|
+
"""
|
|
188
|
+
return getattr(self._local, "deadline", None)
|
|
189
|
+
|
|
190
|
+
def _deadline_exceeded(self) -> bool:
|
|
191
|
+
"""Whether this thread's wall-clock budget has run out."""
|
|
192
|
+
deadline = self._deadline
|
|
193
|
+
return deadline is not None and time.monotonic() >= deadline
|
|
194
|
+
|
|
195
|
+
def _check_budget_at_wait(self) -> bool:
|
|
196
|
+
"""WAIT-entry budget enforcement; True when a budget tripped.
|
|
197
|
+
|
|
198
|
+
Sub-agent exhaustion keeps the historical soft path (``info[
|
|
199
|
+
"budget"]`` -> DONE with a best-effort final answer). An
|
|
200
|
+
opted-in top-level budget (headless/CI) is a hard stop: it
|
|
201
|
+
routes ERRS with an explicit error so the driving process gets
|
|
202
|
+
a failed exit code instead of silently truncated work.
|
|
203
|
+
"""
|
|
204
|
+
if self.max_rounds is not None and self.rounds >= self.max_rounds:
|
|
205
|
+
if self.top_level:
|
|
206
|
+
self.error = "Error: round budget exhausted before the run finished"
|
|
207
|
+
self.info["error"] = self.error
|
|
208
|
+
self.session.notify("error", self.error)
|
|
209
|
+
else:
|
|
210
|
+
self.info["budget"] = True
|
|
211
|
+
return True
|
|
212
|
+
if self._deadline_exceeded():
|
|
213
|
+
self.error = f"Error: {_deadline_message(self.timeout)}"
|
|
214
|
+
self.info["error"] = self.error
|
|
215
|
+
self.session.notify("error", self.error)
|
|
216
|
+
return True
|
|
217
|
+
return False
|
|
218
|
+
|
|
159
219
|
# ------------------------------------------------------------------
|
|
160
220
|
# finite state machine
|
|
161
221
|
#
|
|
@@ -346,6 +406,11 @@ class AgentLoop:
|
|
|
346
406
|
handler.
|
|
347
407
|
"""
|
|
348
408
|
session = self.session
|
|
409
|
+
if self.timeout is not None:
|
|
410
|
+
# Wall-clock budget for THIS run's thread. Thread-local on
|
|
411
|
+
# purpose: sub-agent loops run in sibling threads and must
|
|
412
|
+
# not inherit (or clear) the parent's deadline.
|
|
413
|
+
self._local.deadline = time.monotonic() + self.timeout
|
|
349
414
|
try:
|
|
350
415
|
# The machine starts directly in WAIT — an asynchronous
|
|
351
416
|
# INIT state is not needed for the synchronous driver.
|
|
@@ -420,8 +485,7 @@ class AgentLoop:
|
|
|
420
485
|
"""
|
|
421
486
|
session = self.session
|
|
422
487
|
self.info.clear()
|
|
423
|
-
if self.
|
|
424
|
-
self.info["budget"] = True
|
|
488
|
+
if self._check_budget_at_wait():
|
|
425
489
|
return
|
|
426
490
|
self.rounds += 1
|
|
427
491
|
if self._is_cancelled():
|
|
@@ -506,6 +570,18 @@ class AgentLoop:
|
|
|
506
570
|
if self._is_cancelled():
|
|
507
571
|
return # response arrived after cancel: drop it
|
|
508
572
|
|
|
573
|
+
# Usage accounting: every loop under this session (main and
|
|
574
|
+
# sub-agents alike — sub-agent tokens spend the same API key)
|
|
575
|
+
# adds its per-round usage; a run's driver reads the totals
|
|
576
|
+
# when the run finishes. Sessions without the attribute (test
|
|
577
|
+
# doubles) skip silently.
|
|
578
|
+
totals = getattr(session, "usage_totals", None)
|
|
579
|
+
if isinstance(totals, dict):
|
|
580
|
+
with totals.setdefault("_lock", threading.Lock()):
|
|
581
|
+
totals["input"] = totals.get("input", 0) + usage.input_tokens
|
|
582
|
+
totals["output"] = totals.get("output", 0) + usage.output_tokens
|
|
583
|
+
totals["rounds"] = totals.get("rounds", 0) + 1
|
|
584
|
+
|
|
509
585
|
# persist the assistant response in the conversation history
|
|
510
586
|
# (text and/or tool calls) so later turns and the UI see it;
|
|
511
587
|
# always append to maintain role alternation — a missing
|
|
@@ -636,11 +712,17 @@ def run_agent_loop(
|
|
|
636
712
|
system: str | None = None,
|
|
637
713
|
max_rounds: int = 60,
|
|
638
714
|
client: LLMClient | None = None,
|
|
715
|
+
budget_top_level: bool = False,
|
|
716
|
+
timeout: float | None = None,
|
|
639
717
|
) -> str | None:
|
|
640
718
|
"""Convenience wrapper running a full agent run (FSM).
|
|
641
719
|
|
|
642
720
|
``client`` (when given) overrides the session's client for this
|
|
643
|
-
run — the per-invocation sub-agent clone.
|
|
721
|
+
run — the per-invocation sub-agent clone. ``budget_top_level``
|
|
722
|
+
opts an unattended top-level run into the ``max_rounds`` budget;
|
|
723
|
+
``timeout`` adds a wall-clock limit (both default off, matching
|
|
724
|
+
the interactive TUI behavior).
|
|
725
|
+
"""
|
|
644
726
|
loop = AgentLoop(
|
|
645
727
|
session,
|
|
646
728
|
messages=messages,
|
|
@@ -648,6 +730,8 @@ def run_agent_loop(
|
|
|
648
730
|
system=system,
|
|
649
731
|
max_rounds=max_rounds,
|
|
650
732
|
client=client,
|
|
733
|
+
budget_top_level=budget_top_level,
|
|
734
|
+
timeout=timeout,
|
|
651
735
|
)
|
|
652
736
|
result = loop.run()
|
|
653
737
|
# Notify run_done only for a successfully completed top-level run:
|
{python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.0}/python_agent_harness/entry/cli.py
RENAMED
|
@@ -4,6 +4,8 @@ Commands:
|
|
|
4
4
|
run interactive TUI agent session (default)
|
|
5
5
|
headless [prompt] non-interactive: submit one prompt, print the result
|
|
6
6
|
(--json emits the run as JSON lines instead)
|
|
7
|
+
serve resident JSONL server over stdin/stdout: submit
|
|
8
|
+
runs repeatedly, answer mid-run questions, cancel
|
|
7
9
|
config [--init] show effective LLM config / write a template file
|
|
8
10
|
|
|
9
11
|
Custom commands (prompts/commands/*.md) — like init, review,
|
|
@@ -207,11 +209,45 @@ def cmd_headless(args: argparse.Namespace) -> int:
|
|
|
207
209
|
if prompt is None:
|
|
208
210
|
prompt = sys.stdin.read()
|
|
209
211
|
runner = run_headless_jsonl if getattr(args, "json", False) else run_headless
|
|
212
|
+
# Unattended budgets: explicit flags win over the config file's
|
|
213
|
+
# headless section (0/negative disables the budget entirely).
|
|
214
|
+
cfg_max_rounds, cfg_timeout = config.load_headless_limits(args.config)
|
|
215
|
+
max_rounds = getattr(args, "max_rounds", None)
|
|
216
|
+
if max_rounds is None:
|
|
217
|
+
max_rounds = cfg_max_rounds
|
|
218
|
+
elif max_rounds <= 0:
|
|
219
|
+
max_rounds = None
|
|
220
|
+
timeout = getattr(args, "timeout", None)
|
|
221
|
+
if timeout is None:
|
|
222
|
+
timeout = cfg_timeout
|
|
223
|
+
elif timeout <= 0:
|
|
224
|
+
timeout = None
|
|
210
225
|
return runner(
|
|
211
226
|
session,
|
|
212
227
|
prompt,
|
|
213
228
|
restore=getattr(args, "restore", None),
|
|
214
229
|
model=getattr(args, "model", None),
|
|
230
|
+
run_id=getattr(args, "run_id", None),
|
|
231
|
+
max_rounds=max_rounds,
|
|
232
|
+
timeout=timeout,
|
|
233
|
+
)
|
|
234
|
+
finally:
|
|
235
|
+
session.close()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def cmd_serve(args: argparse.Namespace) -> int:
|
|
239
|
+
project_dir = getattr(args, "project", None) or os.getcwd()
|
|
240
|
+
session = make_session_with_mcp(
|
|
241
|
+
project_dir,
|
|
242
|
+
config_path=args.config,
|
|
243
|
+
stream=False if getattr(args, "no_stream", False) else None,
|
|
244
|
+
)
|
|
245
|
+
try:
|
|
246
|
+
from .server import run_serve
|
|
247
|
+
|
|
248
|
+
return run_serve(
|
|
249
|
+
session,
|
|
250
|
+
answer_timeout=float(getattr(args, "answer_timeout", 0.0) or 0.0),
|
|
215
251
|
)
|
|
216
252
|
finally:
|
|
217
253
|
session.close()
|
|
@@ -299,6 +335,15 @@ def cmd_config(args: argparse.Namespace) -> int:
|
|
|
299
335
|
# Show default agent
|
|
300
336
|
default_agent = config.load_default_agent(args.path)
|
|
301
337
|
print(f"default_agent: {default_agent or '(default: agent.md)'}")
|
|
338
|
+
max_rounds, timeout = config.load_headless_limits(args.path)
|
|
339
|
+
if max_rounds is None and timeout is None:
|
|
340
|
+
print(
|
|
341
|
+
"headless: (unlimited — set headless.max_rounds/timeout or use --max-rounds/--timeout)"
|
|
342
|
+
)
|
|
343
|
+
else:
|
|
344
|
+
print(
|
|
345
|
+
f"headless: max_rounds={max_rounds or '(unlimited)'}, timeout={timeout or '(unlimited)'}s"
|
|
346
|
+
)
|
|
302
347
|
return 0
|
|
303
348
|
|
|
304
349
|
|
|
@@ -372,11 +417,57 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
372
417
|
help="emit the run as JSON lines on stdout (start/delta/notify/log/"
|
|
373
418
|
"result events) for programmatic driving; diagnostics stay on stderr",
|
|
374
419
|
)
|
|
420
|
+
p_headless.add_argument(
|
|
421
|
+
"--run-id",
|
|
422
|
+
metavar="ID",
|
|
423
|
+
default=None,
|
|
424
|
+
help="correlation id echoed on every --json line (e.g. the controller's exec id)",
|
|
425
|
+
)
|
|
426
|
+
p_headless.add_argument(
|
|
427
|
+
"--max-rounds",
|
|
428
|
+
metavar="N",
|
|
429
|
+
type=int,
|
|
430
|
+
default=None,
|
|
431
|
+
help="cap the run at N LLM rounds (unattended budget; default: config "
|
|
432
|
+
"headless.max_rounds, else unlimited)",
|
|
433
|
+
)
|
|
434
|
+
p_headless.add_argument(
|
|
435
|
+
"--timeout",
|
|
436
|
+
metavar="SECONDS",
|
|
437
|
+
type=float,
|
|
438
|
+
default=None,
|
|
439
|
+
help="wall-clock limit for the run in seconds (unattended budget; "
|
|
440
|
+
"default: config headless.timeout, else unlimited)",
|
|
441
|
+
)
|
|
375
442
|
|
|
376
443
|
p_config = sub.add_parser("config", help="show effective LLM config or write a template file")
|
|
377
444
|
p_config.add_argument("--init", action="store_true", help="write a config template")
|
|
378
445
|
p_config.add_argument("--path", metavar="PATH", help="config file path")
|
|
379
446
|
p_config.set_defaults(func=cmd_config)
|
|
447
|
+
|
|
448
|
+
p_serve = sub.add_parser(
|
|
449
|
+
"serve",
|
|
450
|
+
help="resident JSONL server: multiple runs per process, mid-run Q&A",
|
|
451
|
+
)
|
|
452
|
+
_add_config_arg(p_serve, suppress=True)
|
|
453
|
+
p_serve.add_argument(
|
|
454
|
+
"--no-stream",
|
|
455
|
+
action="store_true",
|
|
456
|
+
help="disable streaming (one-shot responses; overrides config file)",
|
|
457
|
+
)
|
|
458
|
+
p_serve.add_argument(
|
|
459
|
+
"--project",
|
|
460
|
+
metavar="DIR",
|
|
461
|
+
help="project directory (default: cwd)",
|
|
462
|
+
)
|
|
463
|
+
p_serve.add_argument(
|
|
464
|
+
"--answer-timeout",
|
|
465
|
+
metavar="SECONDS",
|
|
466
|
+
type=float,
|
|
467
|
+
default=0.0,
|
|
468
|
+
help="give up waiting for a host answer after N seconds (0 = wait "
|
|
469
|
+
'forever, the default; the host can answer via {"op": "answer"})',
|
|
470
|
+
)
|
|
380
471
|
return parser
|
|
381
472
|
|
|
382
473
|
|
|
@@ -387,6 +478,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
387
478
|
return cmd_run(args)
|
|
388
479
|
if args.command == "headless":
|
|
389
480
|
return cmd_headless(args)
|
|
481
|
+
if args.command == "serve":
|
|
482
|
+
return cmd_serve(args)
|
|
390
483
|
if args.command == "config":
|
|
391
484
|
return cmd_config(args)
|
|
392
485
|
parser.print_help()
|
|
@@ -219,6 +219,8 @@ class Controller:
|
|
|
219
219
|
text: str | Message,
|
|
220
220
|
system: str | None = None,
|
|
221
221
|
restore: Any = None,
|
|
222
|
+
max_rounds: int | None = None,
|
|
223
|
+
timeout: float | None = None,
|
|
222
224
|
) -> RunHandle | None:
|
|
223
225
|
"""Start an agent run in a worker thread; return its handle.
|
|
224
226
|
|
|
@@ -228,7 +230,10 @@ class Controller:
|
|
|
228
230
|
directly). SYSTEM overrides the session's system prompt for
|
|
229
231
|
this run only. RESTORE (if given) runs when the run finishes
|
|
230
232
|
— used by slash commands to put back state they borrowed
|
|
231
|
-
(e.g. project_dir).
|
|
233
|
+
(e.g. project_dir). MAX_ROUNDS (when given) opts this
|
|
234
|
+
unattended run into a round budget; TIMEOUT sets a wall-clock
|
|
235
|
+
limit — both default to the interactive behavior (unbounded),
|
|
236
|
+
and both apply to the top-level loop only.
|
|
232
237
|
|
|
233
238
|
Returns None when the text was only failed ``@file``
|
|
234
239
|
references with nothing else to send (the view surfaces the
|
|
@@ -249,9 +254,19 @@ class Controller:
|
|
|
249
254
|
self.session.run_generation += 1
|
|
250
255
|
self.session.cancel_event.clear()
|
|
251
256
|
seq = self.session.run_generation
|
|
257
|
+
# Fresh usage accounting for this run (the previous run's totals
|
|
258
|
+
# were already emitted on its result line): a fresh dict, not a
|
|
259
|
+
# clear(), so a late sub-agent still writing into the old dict
|
|
260
|
+
# cannot pollute this run's numbers.
|
|
261
|
+
self.session.usage_totals = {
|
|
262
|
+
"input": 0,
|
|
263
|
+
"output": 0,
|
|
264
|
+
"rounds": 0,
|
|
265
|
+
"_lock": threading.Lock(),
|
|
266
|
+
}
|
|
252
267
|
worker = threading.Thread(
|
|
253
268
|
target=self._run_worker,
|
|
254
|
-
args=(user_msg, seq, system, restore),
|
|
269
|
+
args=(user_msg, seq, system, restore, max_rounds, timeout),
|
|
255
270
|
daemon=True,
|
|
256
271
|
)
|
|
257
272
|
worker.start()
|
|
@@ -324,6 +339,8 @@ class Controller:
|
|
|
324
339
|
seq: int,
|
|
325
340
|
system: str | None = None,
|
|
326
341
|
restore: Any = None,
|
|
342
|
+
max_rounds: int | None = None,
|
|
343
|
+
timeout: float | None = None,
|
|
327
344
|
) -> None:
|
|
328
345
|
"""Worker-thread body of a submitted run: drive the agent loop.
|
|
329
346
|
|
|
@@ -342,6 +359,9 @@ class Controller:
|
|
|
342
359
|
messages=list(self.conversation_history),
|
|
343
360
|
top_level=True,
|
|
344
361
|
system=system or self.system_prompt,
|
|
362
|
+
budget_top_level=max_rounds is not None,
|
|
363
|
+
max_rounds=max_rounds if max_rounds is not None else 60,
|
|
364
|
+
timeout=timeout,
|
|
345
365
|
)
|
|
346
366
|
if seq == self.session.run_generation and self.session.last_messages:
|
|
347
367
|
self.conversation_history = list(self.session.last_messages)
|