python-agent-harness 1.5.5.9__tar.gz → 1.5.6.1__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.1}/PKG-INFO +126 -7
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/README.md +124 -5
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/pyproject.toml +2 -6
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/agent.py +133 -7
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/models.py +33 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/cli.py +140 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/controller.py +37 -2
- python_agent_harness-1.5.6.1/python_agent_harness/entry/headless.py +689 -0
- python_agent_harness-1.5.6.1/python_agent_harness/entry/server.py +1003 -0
- python_agent_harness-1.5.6.1/python_agent_harness/entry/view.py +110 -0
- python_agent_harness-1.5.6.1/python_agent_harness/io/text_filter.py +262 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/llm/client.py +2 -12
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/agent.md +1 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/subagent.md +1 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/session/config.py +77 -14
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/session/session.py +74 -9
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/__init__.py +4 -16
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/base.py +1 -1
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/bash.py +1 -3
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/diffapply.py +8 -3
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/edit.py +3 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/filesystem.py +97 -6
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/insert.py +3 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/lsp.py +0 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/read.py +0 -1
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/write.py +8 -6
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/commands.py +1 -1
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/core.py +16 -2
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/input.py +11 -16
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/render.py +2 -26
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1/python_agent_harness.egg-info}/PKG-INFO +126 -7
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/SOURCES.txt +1 -4
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/requires.txt +1 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/tests/test_invariants.py +1 -1
- python_agent_harness-1.5.5.9/python_agent_harness/entry/headless.py +0 -350
- python_agent_harness-1.5.5.9/python_agent_harness/entry/view.py +0 -51
- python_agent_harness-1.5.5.9/python_agent_harness/io/text_filter.py +0 -84
- 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.1}/LICENSE +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/context_manager.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/planmode.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/subagent.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/token_estimator.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/tool_runner.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/io/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/io/attachments.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/io/diffrender.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/io/persistence.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/llm/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/client.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/config.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/manager.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/agents/reviewer.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/core.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/session/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/session/commands.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/glob.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/glob_mac.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/grep.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/grep_mac.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/setup.cfg +0 -0
- {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/tests/test_scenarios.py +0 -0
{python_agent_harness-1.5.5.9/python_agent_harness.egg-info → python_agent_harness-1.5.6.1}/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.1
|
|
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
|
|
|
@@ -221,6 +222,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
221
222
|
- **`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.
|
|
222
223
|
- **`lsp.servers`** — optional per-extension LSP server overrides for the `LSP` code-intelligence tool. Keys are file extensions (e.g. `.py`, `.cpp`); each value has a `command` (the server argv) and an optional `language_id` (defaults to the extension without its dot). These layer on top of the built-in `DEFAULT_SERVERS` table in `lsp/manager.py`; an entry for an existing extension replaces its default. The server binary must be on `PATH`.
|
|
223
224
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
225
|
+
- **`headless`** — optional server-side limits for unattended `headless` runs: `max_rounds` caps LLM rounds and `timeout` caps wall-clock seconds. `--max-rounds` / `--timeout` override these per call; values `<= 0` or `null` disable. Unset = unlimited (interactive behavior).
|
|
226
|
+
- **`serve`** — the same two keys for the resident `serve` server, applied to **each** run rather than the process: the server is resident, so every `submit` gets a fresh budget. Enforced in-process so a driving host cannot raise its own ceiling — the ceiling belongs to whoever starts the sandbox. A tripped budget ends the run with a normal `result` line carrying `errors[].code` of `budget` or `timeout` plus the token usage consumed. `--max-rounds` / `--timeout` override these; values `<= 0` or `null` disable. Unset = unlimited.
|
|
224
227
|
- **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
|
|
225
228
|
- **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
|
|
226
229
|
- **LLM logging** — request and response bodies are logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change the directory. The log path is printed at startup.
|
|
@@ -270,9 +273,16 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
270
273
|
one `{"type": ...}` object per line: `start` (echoes the prompt and
|
|
271
274
|
submit warnings), `delta` (streamed text chunks), `notify` (tool and
|
|
272
275
|
status events, with `kind`/`data`), `log`, and a final `result` (the
|
|
273
|
-
filtered answer plus any `errors`).
|
|
274
|
-
|
|
275
|
-
|
|
276
|
+
filtered answer plus any `errors`). Every line carries `protocol`
|
|
277
|
+
(the wire schema version — reject an unknown version before
|
|
278
|
+
consuming events) and `seq` (a per-stream monotonic counter for
|
|
279
|
+
drop/reorder detection). The `result` line reports `errors` as a
|
|
280
|
+
structured list of `{"code", "message"}` objects (codes: `budget`,
|
|
281
|
+
`timeout`, `nothing`, `restore`, `no_answer`, `unknown`, ...) with an
|
|
282
|
+
`error_messages` flat-string mirror for simple drivers. Diagnostics
|
|
283
|
+
(restore/model notes, a plain-text echo of error events) still go to
|
|
284
|
+
stderr, so one pipe carries the structured stream. Exit codes are
|
|
285
|
+
unchanged.
|
|
276
286
|
|
|
277
287
|
```sh
|
|
278
288
|
python-agent-harness headless "fix it" --json | jq -c 'select(.type=="result")'
|
|
@@ -281,6 +291,113 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
281
291
|
- Exit code is 0 on success, 1 when the prompt was empty (only failed
|
|
282
292
|
`@file` references), the run raised an agent error, or the restore failed.
|
|
283
293
|
|
|
294
|
+
### Serve mode (resident JSONL server)
|
|
295
|
+
|
|
296
|
+
```sh
|
|
297
|
+
python-agent-harness serve [--project DIR] [--answer-timeout SECONDS]
|
|
298
|
+
[--max-rounds N] [--timeout SECONDS]
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
A persistent, bidirectional runtime boundary for hosting applications
|
|
302
|
+
(a web backend, an IDE, a CI driver). Unlike `headless --json` (one
|
|
303
|
+
prompt per process, write-only stream), `serve` keeps the
|
|
304
|
+
`Controller`/`Session` resident and speaks a request/response protocol
|
|
305
|
+
over stdin/stdout — the same process boundary (containerizable), but
|
|
306
|
+
the host can:
|
|
307
|
+
|
|
308
|
+
- submit multiple prompts over the process's lifetime — no per-turn
|
|
309
|
+
interpreter spawn, and conversation history is retained between them
|
|
310
|
+
(multi-turn memory);
|
|
311
|
+
- answer the agent's mid-run questions (the `Question` tool and
|
|
312
|
+
plan-exit confirmation) via an `answer` op;
|
|
313
|
+
- cancel a run as a protocol message (no signal semantics).
|
|
314
|
+
|
|
315
|
+
Protocol (one JSON object per line):
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
host → agent: {"op": "submit", "prompt": ..., "run_id": ...}
|
|
319
|
+
{"op": "answer", "run_id": ..., "answers": [...], "ask_id": ...}
|
|
320
|
+
{"op": "cancel", "run_id": ...}
|
|
321
|
+
{"op": "hello", "protocol_versions": [...]}
|
|
322
|
+
{"op": "ping"} | {"op": "shutdown"}
|
|
323
|
+
... any op may carry "op_id", echoed on what it causes
|
|
324
|
+
agent → host: {"protocol": V, "type": "ready", "pid": ...,
|
|
325
|
+
"protocol_version": V, "capabilities": [...]} first line
|
|
326
|
+
{"protocol": V, "type": "hello", "protocol_version": V,
|
|
327
|
+
"capabilities": [...], "op_id": ...} negotiation reply
|
|
328
|
+
{"protocol": V, "seq": N, "type": "start"|"delta"|"notify"|"log",
|
|
329
|
+
"run_id": ...}
|
|
330
|
+
{"protocol": V, "seq": N, "type": "result", "run_id": ...,
|
|
331
|
+
"answer": ..., "errors": [{"code", "message"}],
|
|
332
|
+
"error_messages": [...], "usage": {...}, "cancelled": bool}
|
|
333
|
+
{"protocol": V, "type": "error",
|
|
334
|
+
"error": {"code": "protocol", "message": ...},
|
|
335
|
+
"message": ...} protocol failures
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Version negotiation runs both ways. `ready` announces what the build
|
|
339
|
+
speaks — a single integer a host can only accept or reject wholesale.
|
|
340
|
+
`hello` is the other direction: the host states the versions *it* can
|
|
341
|
+
parse and the server confirms a shared one or refuses with a
|
|
342
|
+
`protocol` error, so a mismatch is settled once, before any run,
|
|
343
|
+
rather than surfacing as misparsed events. With one line shape today
|
|
344
|
+
the only outcomes are "agreed" or "no overlap", but it is the hook a
|
|
345
|
+
future version needs in order to downgrade instead of breaking an
|
|
346
|
+
older host. Both halves are advertised as the `hello` capability.
|
|
347
|
+
|
|
348
|
+
There is no generic `ack`: each op is acknowledged by its own effect
|
|
349
|
+
on the stream. Failures are the exception, because an effect that
|
|
350
|
+
never happened cannot be correlated — so an op may carry an `op_id`,
|
|
351
|
+
echoed on the `error` (or `pong`) it causes. A host that pipelined an
|
|
352
|
+
`answer` and a `cancel` can then tell which one was refused instead of
|
|
353
|
+
seeing two indistinguishable error lines. Advertised as the `op_id`
|
|
354
|
+
capability; omitting the field keeps the older behaviour.
|
|
355
|
+
|
|
356
|
+
A mid-run question arrives as a `notify` with `kind: "ask"` (data has
|
|
357
|
+
`kind: "ask"|"confirm"` and an `ask_id`); reply with `answer`, echoing
|
|
358
|
+
the `ask_id` so the reply is matched to its question. A mismatched id
|
|
359
|
+
is refused rather than applied to whatever is pending — otherwise a
|
|
360
|
+
reply sent for a question that has since timed out would resolve the
|
|
361
|
+
*next* one. Omitting `ask_id` keeps the older behaviour, except once
|
|
362
|
+
some ask in the run has timed out, when an uncorrelated answer is
|
|
363
|
+
ambiguous and is refused.
|
|
364
|
+
|
|
365
|
+
One run at a time; a `submit` while one is active is rejected with an
|
|
366
|
+
`error` line. `--answer-timeout SECONDS` bounds how long a pending
|
|
367
|
+
question waits for the host's answer (default: forever). The result
|
|
368
|
+
line's shape is identical to `headless --json`'s, so a driver can
|
|
369
|
+
speak both protocols with one parser.
|
|
370
|
+
|
|
371
|
+
`ready` carries a `capabilities` list naming the features the build
|
|
372
|
+
supports, so a host can adapt instead of inferring a feature's absence
|
|
373
|
+
from events that never arrive. A `notify` of kind `usage` reports the
|
|
374
|
+
run's running `{input, output, rounds}` after each round, so a host can
|
|
375
|
+
meter mid-run and `cancel` a run that outruns its budget rather than
|
|
376
|
+
learning the cost only from the terminal `result`. Classified
|
|
377
|
+
failures carry their code from the raise site (`errors[].code` of
|
|
378
|
+
`budget` or `timeout`), so a driver never has to match words in a
|
|
379
|
+
human-readable message.
|
|
380
|
+
|
|
381
|
+
`--max-rounds N` and `--timeout SECONDS` (or the config file's `serve`
|
|
382
|
+
section) bound **each** run rather than the process — the resident
|
|
383
|
+
server grants every `submit` a fresh budget. Both are off by default.
|
|
384
|
+
They are deliberately not fields on the `submit` op: `serve` sandboxes
|
|
385
|
+
untrusted agent code on a host's behalf, so the ceiling belongs to
|
|
386
|
+
whoever starts the sandbox, not to the caller. A tripped budget
|
|
387
|
+
unwinds the agent loop through its normal path, so the run still ends
|
|
388
|
+
with a `result` line carrying `errors[].code` of `budget` or `timeout`
|
|
389
|
+
plus the token usage consumed — a host bills and reports it like any
|
|
390
|
+
other outcome.
|
|
391
|
+
|
|
392
|
+
`timeout` is **cooperative** — checked between rounds, not by a
|
|
393
|
+
watchdog — so it bounds work at those checkpoints but does not
|
|
394
|
+
forcibly interrupt a blocking operation mid-round (a hung tool, a slow
|
|
395
|
+
network read, or a run parked on a mid-run question awaiting a human
|
|
396
|
+
answer). Use `--answer-timeout` to cap how long an ask waits, and rely
|
|
397
|
+
on the hosting process's own exec watchdog (which sends `cancel` then
|
|
398
|
+
kills) as the hard wall-clock stop for anything the cooperative budget
|
|
399
|
+
cannot reach.
|
|
400
|
+
|
|
284
401
|
### Slash commands
|
|
285
402
|
|
|
286
403
|
| Command | Description |
|
|
@@ -297,6 +414,7 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
297
414
|
| `/clear` | Start a fresh conversation |
|
|
298
415
|
| `/model [name]` | Switch LLM profiles; `default` restores the session's original model |
|
|
299
416
|
| `/agent [name]` | Switch agent system prompt; `default` restores the built-in `agent.md` |
|
|
417
|
+
| `/help` | List the available commands |
|
|
300
418
|
| `/exit` | Quit |
|
|
301
419
|
|
|
302
420
|
Custom commands from `prompts/commands/*.md` are registered as slash commands as well (TUI only).
|
|
@@ -335,18 +453,19 @@ You are a personal assistant. You do NOT modify files or run shell commands.
|
|
|
335
453
|
|
|
336
454
|
## Development
|
|
337
455
|
|
|
338
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux
|
|
456
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and MacOS.
|
|
339
457
|
|
|
340
458
|
```sh
|
|
341
459
|
make test # unit tests
|
|
342
460
|
venv/bin/pip install -e ".[dev]" # development tools
|
|
343
461
|
venv/bin/ruff check . # lint
|
|
462
|
+
venv/bin/ruff format --check . # formatting (CI blocks on this too)
|
|
344
463
|
venv/bin/pyright # type checking
|
|
345
464
|
venv/bin/python -m build # build sdist + wheel
|
|
346
465
|
venv/bin/pip-audit # dependency audit
|
|
347
466
|
```
|
|
348
467
|
|
|
349
|
-
CI blocks on Ruff and Pyright failures.
|
|
468
|
+
CI blocks on Ruff (lint **and** `format --check`) and Pyright failures.
|
|
350
469
|
|
|
351
470
|
## Design philosophy
|
|
352
471
|
|
|
@@ -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
|
|
|
@@ -187,6 +188,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
187
188
|
- **`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.
|
|
188
189
|
- **`lsp.servers`** — optional per-extension LSP server overrides for the `LSP` code-intelligence tool. Keys are file extensions (e.g. `.py`, `.cpp`); each value has a `command` (the server argv) and an optional `language_id` (defaults to the extension without its dot). These layer on top of the built-in `DEFAULT_SERVERS` table in `lsp/manager.py`; an entry for an existing extension replaces its default. The server binary must be on `PATH`.
|
|
189
190
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
191
|
+
- **`headless`** — optional server-side limits for unattended `headless` runs: `max_rounds` caps LLM rounds and `timeout` caps wall-clock seconds. `--max-rounds` / `--timeout` override these per call; values `<= 0` or `null` disable. Unset = unlimited (interactive behavior).
|
|
192
|
+
- **`serve`** — the same two keys for the resident `serve` server, applied to **each** run rather than the process: the server is resident, so every `submit` gets a fresh budget. Enforced in-process so a driving host cannot raise its own ceiling — the ceiling belongs to whoever starts the sandbox. A tripped budget ends the run with a normal `result` line carrying `errors[].code` of `budget` or `timeout` plus the token usage consumed. `--max-rounds` / `--timeout` override these; values `<= 0` or `null` disable. Unset = unlimited.
|
|
190
193
|
- **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
|
|
191
194
|
- **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
|
|
192
195
|
- **LLM logging** — request and response bodies are logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change the directory. The log path is printed at startup.
|
|
@@ -236,9 +239,16 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
236
239
|
one `{"type": ...}` object per line: `start` (echoes the prompt and
|
|
237
240
|
submit warnings), `delta` (streamed text chunks), `notify` (tool and
|
|
238
241
|
status events, with `kind`/`data`), `log`, and a final `result` (the
|
|
239
|
-
filtered answer plus any `errors`).
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
filtered answer plus any `errors`). Every line carries `protocol`
|
|
243
|
+
(the wire schema version — reject an unknown version before
|
|
244
|
+
consuming events) and `seq` (a per-stream monotonic counter for
|
|
245
|
+
drop/reorder detection). The `result` line reports `errors` as a
|
|
246
|
+
structured list of `{"code", "message"}` objects (codes: `budget`,
|
|
247
|
+
`timeout`, `nothing`, `restore`, `no_answer`, `unknown`, ...) with an
|
|
248
|
+
`error_messages` flat-string mirror for simple drivers. Diagnostics
|
|
249
|
+
(restore/model notes, a plain-text echo of error events) still go to
|
|
250
|
+
stderr, so one pipe carries the structured stream. Exit codes are
|
|
251
|
+
unchanged.
|
|
242
252
|
|
|
243
253
|
```sh
|
|
244
254
|
python-agent-harness headless "fix it" --json | jq -c 'select(.type=="result")'
|
|
@@ -247,6 +257,113 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
247
257
|
- Exit code is 0 on success, 1 when the prompt was empty (only failed
|
|
248
258
|
`@file` references), the run raised an agent error, or the restore failed.
|
|
249
259
|
|
|
260
|
+
### Serve mode (resident JSONL server)
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
python-agent-harness serve [--project DIR] [--answer-timeout SECONDS]
|
|
264
|
+
[--max-rounds N] [--timeout SECONDS]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
A persistent, bidirectional runtime boundary for hosting applications
|
|
268
|
+
(a web backend, an IDE, a CI driver). Unlike `headless --json` (one
|
|
269
|
+
prompt per process, write-only stream), `serve` keeps the
|
|
270
|
+
`Controller`/`Session` resident and speaks a request/response protocol
|
|
271
|
+
over stdin/stdout — the same process boundary (containerizable), but
|
|
272
|
+
the host can:
|
|
273
|
+
|
|
274
|
+
- submit multiple prompts over the process's lifetime — no per-turn
|
|
275
|
+
interpreter spawn, and conversation history is retained between them
|
|
276
|
+
(multi-turn memory);
|
|
277
|
+
- answer the agent's mid-run questions (the `Question` tool and
|
|
278
|
+
plan-exit confirmation) via an `answer` op;
|
|
279
|
+
- cancel a run as a protocol message (no signal semantics).
|
|
280
|
+
|
|
281
|
+
Protocol (one JSON object per line):
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
host → agent: {"op": "submit", "prompt": ..., "run_id": ...}
|
|
285
|
+
{"op": "answer", "run_id": ..., "answers": [...], "ask_id": ...}
|
|
286
|
+
{"op": "cancel", "run_id": ...}
|
|
287
|
+
{"op": "hello", "protocol_versions": [...]}
|
|
288
|
+
{"op": "ping"} | {"op": "shutdown"}
|
|
289
|
+
... any op may carry "op_id", echoed on what it causes
|
|
290
|
+
agent → host: {"protocol": V, "type": "ready", "pid": ...,
|
|
291
|
+
"protocol_version": V, "capabilities": [...]} first line
|
|
292
|
+
{"protocol": V, "type": "hello", "protocol_version": V,
|
|
293
|
+
"capabilities": [...], "op_id": ...} negotiation reply
|
|
294
|
+
{"protocol": V, "seq": N, "type": "start"|"delta"|"notify"|"log",
|
|
295
|
+
"run_id": ...}
|
|
296
|
+
{"protocol": V, "seq": N, "type": "result", "run_id": ...,
|
|
297
|
+
"answer": ..., "errors": [{"code", "message"}],
|
|
298
|
+
"error_messages": [...], "usage": {...}, "cancelled": bool}
|
|
299
|
+
{"protocol": V, "type": "error",
|
|
300
|
+
"error": {"code": "protocol", "message": ...},
|
|
301
|
+
"message": ...} protocol failures
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Version negotiation runs both ways. `ready` announces what the build
|
|
305
|
+
speaks — a single integer a host can only accept or reject wholesale.
|
|
306
|
+
`hello` is the other direction: the host states the versions *it* can
|
|
307
|
+
parse and the server confirms a shared one or refuses with a
|
|
308
|
+
`protocol` error, so a mismatch is settled once, before any run,
|
|
309
|
+
rather than surfacing as misparsed events. With one line shape today
|
|
310
|
+
the only outcomes are "agreed" or "no overlap", but it is the hook a
|
|
311
|
+
future version needs in order to downgrade instead of breaking an
|
|
312
|
+
older host. Both halves are advertised as the `hello` capability.
|
|
313
|
+
|
|
314
|
+
There is no generic `ack`: each op is acknowledged by its own effect
|
|
315
|
+
on the stream. Failures are the exception, because an effect that
|
|
316
|
+
never happened cannot be correlated — so an op may carry an `op_id`,
|
|
317
|
+
echoed on the `error` (or `pong`) it causes. A host that pipelined an
|
|
318
|
+
`answer` and a `cancel` can then tell which one was refused instead of
|
|
319
|
+
seeing two indistinguishable error lines. Advertised as the `op_id`
|
|
320
|
+
capability; omitting the field keeps the older behaviour.
|
|
321
|
+
|
|
322
|
+
A mid-run question arrives as a `notify` with `kind: "ask"` (data has
|
|
323
|
+
`kind: "ask"|"confirm"` and an `ask_id`); reply with `answer`, echoing
|
|
324
|
+
the `ask_id` so the reply is matched to its question. A mismatched id
|
|
325
|
+
is refused rather than applied to whatever is pending — otherwise a
|
|
326
|
+
reply sent for a question that has since timed out would resolve the
|
|
327
|
+
*next* one. Omitting `ask_id` keeps the older behaviour, except once
|
|
328
|
+
some ask in the run has timed out, when an uncorrelated answer is
|
|
329
|
+
ambiguous and is refused.
|
|
330
|
+
|
|
331
|
+
One run at a time; a `submit` while one is active is rejected with an
|
|
332
|
+
`error` line. `--answer-timeout SECONDS` bounds how long a pending
|
|
333
|
+
question waits for the host's answer (default: forever). The result
|
|
334
|
+
line's shape is identical to `headless --json`'s, so a driver can
|
|
335
|
+
speak both protocols with one parser.
|
|
336
|
+
|
|
337
|
+
`ready` carries a `capabilities` list naming the features the build
|
|
338
|
+
supports, so a host can adapt instead of inferring a feature's absence
|
|
339
|
+
from events that never arrive. A `notify` of kind `usage` reports the
|
|
340
|
+
run's running `{input, output, rounds}` after each round, so a host can
|
|
341
|
+
meter mid-run and `cancel` a run that outruns its budget rather than
|
|
342
|
+
learning the cost only from the terminal `result`. Classified
|
|
343
|
+
failures carry their code from the raise site (`errors[].code` of
|
|
344
|
+
`budget` or `timeout`), so a driver never has to match words in a
|
|
345
|
+
human-readable message.
|
|
346
|
+
|
|
347
|
+
`--max-rounds N` and `--timeout SECONDS` (or the config file's `serve`
|
|
348
|
+
section) bound **each** run rather than the process — the resident
|
|
349
|
+
server grants every `submit` a fresh budget. Both are off by default.
|
|
350
|
+
They are deliberately not fields on the `submit` op: `serve` sandboxes
|
|
351
|
+
untrusted agent code on a host's behalf, so the ceiling belongs to
|
|
352
|
+
whoever starts the sandbox, not to the caller. A tripped budget
|
|
353
|
+
unwinds the agent loop through its normal path, so the run still ends
|
|
354
|
+
with a `result` line carrying `errors[].code` of `budget` or `timeout`
|
|
355
|
+
plus the token usage consumed — a host bills and reports it like any
|
|
356
|
+
other outcome.
|
|
357
|
+
|
|
358
|
+
`timeout` is **cooperative** — checked between rounds, not by a
|
|
359
|
+
watchdog — so it bounds work at those checkpoints but does not
|
|
360
|
+
forcibly interrupt a blocking operation mid-round (a hung tool, a slow
|
|
361
|
+
network read, or a run parked on a mid-run question awaiting a human
|
|
362
|
+
answer). Use `--answer-timeout` to cap how long an ask waits, and rely
|
|
363
|
+
on the hosting process's own exec watchdog (which sends `cancel` then
|
|
364
|
+
kills) as the hard wall-clock stop for anything the cooperative budget
|
|
365
|
+
cannot reach.
|
|
366
|
+
|
|
250
367
|
### Slash commands
|
|
251
368
|
|
|
252
369
|
| Command | Description |
|
|
@@ -263,6 +380,7 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
263
380
|
| `/clear` | Start a fresh conversation |
|
|
264
381
|
| `/model [name]` | Switch LLM profiles; `default` restores the session's original model |
|
|
265
382
|
| `/agent [name]` | Switch agent system prompt; `default` restores the built-in `agent.md` |
|
|
383
|
+
| `/help` | List the available commands |
|
|
266
384
|
| `/exit` | Quit |
|
|
267
385
|
|
|
268
386
|
Custom commands from `prompts/commands/*.md` are registered as slash commands as well (TUI only).
|
|
@@ -301,18 +419,19 @@ You are a personal assistant. You do NOT modify files or run shell commands.
|
|
|
301
419
|
|
|
302
420
|
## Development
|
|
303
421
|
|
|
304
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux
|
|
422
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and MacOS.
|
|
305
423
|
|
|
306
424
|
```sh
|
|
307
425
|
make test # unit tests
|
|
308
426
|
venv/bin/pip install -e ".[dev]" # development tools
|
|
309
427
|
venv/bin/ruff check . # lint
|
|
428
|
+
venv/bin/ruff format --check . # formatting (CI blocks on this too)
|
|
310
429
|
venv/bin/pyright # type checking
|
|
311
430
|
venv/bin/python -m build # build sdist + wheel
|
|
312
431
|
venv/bin/pip-audit # dependency audit
|
|
313
432
|
```
|
|
314
433
|
|
|
315
|
-
CI blocks on Ruff and Pyright failures.
|
|
434
|
+
CI blocks on Ruff (lint **and** `format --check`) and Pyright failures.
|
|
316
435
|
|
|
317
436
|
## Design philosophy
|
|
318
437
|
|
|
@@ -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.1"
|
|
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
|