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.
Files changed (97) hide show
  1. {python_agent_harness-1.5.5.9/python_agent_harness.egg-info → python_agent_harness-1.5.6.1}/PKG-INFO +126 -7
  2. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/README.md +124 -5
  3. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/pyproject.toml +2 -6
  4. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/__init__.py +1 -1
  5. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/agent.py +133 -7
  6. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/models.py +33 -0
  7. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/cli.py +140 -0
  8. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/controller.py +37 -2
  9. python_agent_harness-1.5.6.1/python_agent_harness/entry/headless.py +689 -0
  10. python_agent_harness-1.5.6.1/python_agent_harness/entry/server.py +1003 -0
  11. python_agent_harness-1.5.6.1/python_agent_harness/entry/view.py +110 -0
  12. python_agent_harness-1.5.6.1/python_agent_harness/io/text_filter.py +262 -0
  13. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/llm/client.py +2 -12
  14. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/agent.md +1 -2
  15. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/subagent.md +1 -2
  16. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/session/config.py +77 -14
  17. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/session/session.py +74 -9
  18. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/__init__.py +4 -16
  19. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/base.py +1 -1
  20. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/bash.py +1 -3
  21. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/diffapply.py +8 -3
  22. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/edit.py +3 -2
  23. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/filesystem.py +97 -6
  24. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/insert.py +3 -2
  25. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/lsp.py +0 -2
  26. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/read.py +0 -1
  27. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/write.py +8 -6
  28. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/commands.py +1 -1
  29. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/core.py +16 -2
  30. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/input.py +11 -16
  31. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/render.py +2 -26
  32. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1/python_agent_harness.egg-info}/PKG-INFO +126 -7
  33. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/SOURCES.txt +1 -4
  34. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/requires.txt +1 -0
  35. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/tests/test_invariants.py +1 -1
  36. python_agent_harness-1.5.5.9/python_agent_harness/entry/headless.py +0 -350
  37. python_agent_harness-1.5.5.9/python_agent_harness/entry/view.py +0 -51
  38. python_agent_harness-1.5.5.9/python_agent_harness/io/text_filter.py +0 -84
  39. python_agent_harness-1.5.5.9/python_agent_harness/tools/bash_win.py +0 -232
  40. python_agent_harness-1.5.5.9/python_agent_harness/tools/edit_win.py +0 -56
  41. python_agent_harness-1.5.5.9/python_agent_harness/tools/glob_win.py +0 -104
  42. python_agent_harness-1.5.5.9/python_agent_harness/tools/grep_win.py +0 -135
  43. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/LICENSE +0 -0
  44. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/__main__.py +0 -0
  45. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/__init__.py +0 -0
  46. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/context_manager.py +0 -0
  47. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/planmode.py +0 -0
  48. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/subagent.py +0 -0
  49. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/token_estimator.py +0 -0
  50. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/core/tool_runner.py +0 -0
  51. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/__init__.py +0 -0
  52. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/io/__init__.py +0 -0
  53. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/io/attachments.py +0 -0
  54. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/io/diffrender.py +0 -0
  55. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/io/persistence.py +0 -0
  56. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/llm/__init__.py +0 -0
  57. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/__init__.py +0 -0
  58. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/client.py +0 -0
  59. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/config.py +0 -0
  60. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/manager.py +0 -0
  61. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/__init__.py +0 -0
  62. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/client.py +0 -0
  63. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/config.py +0 -0
  64. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/manager.py +0 -0
  65. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/__init__.py +0 -0
  66. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/agents/reviewer.md +0 -0
  67. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/build-switch.md +0 -0
  68. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/commands/explain.md +0 -0
  69. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/compact.md +0 -0
  70. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/core.py +0 -0
  71. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/initialize.md +0 -0
  72. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/plan-mode.md +0 -0
  73. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/plan.md +0 -0
  74. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/review.md +0 -0
  75. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/summary.md +0 -0
  76. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/task-completion-rules.md +0 -0
  77. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/title.md +0 -0
  78. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/session/__init__.py +0 -0
  79. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/session/commands.py +0 -0
  80. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/agent_tool.py +0 -0
  81. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/edit_mac.py +0 -0
  82. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/glob.py +0 -0
  83. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/glob_mac.py +0 -0
  84. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/grep.py +0 -0
  85. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/grep_mac.py +0 -0
  86. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/mcp.py +0 -0
  87. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/mkdir.py +0 -0
  88. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/planexit.py +0 -0
  89. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/question.py +0 -0
  90. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/skill.py +0 -0
  91. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/todo.py +0 -0
  92. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/__init__.py +0 -0
  93. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/dependency_links.txt +0 -0
  94. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/entry_points.txt +0 -0
  95. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/top_level.txt +0 -0
  96. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/setup.cfg +0 -0
  97. {python_agent_harness-1.5.5.9 → python_agent_harness-1.5.6.1}/tests/test_scenarios.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-agent-harness
3
- Version: 1.5.5.9
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`). Diagnostics (restore/model notes,
274
- a plain-text echo of error events) still go to stderr, so one pipe
275
- carries the structured stream. Exit codes are unchanged.
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, MacOS and Windows.
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`). Diagnostics (restore/model notes,
240
- a plain-text echo of error events) still go to stderr, so one pipe
241
- carries the structured stream. Exit codes are unchanged.
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, MacOS and Windows.
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.5.9"
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
@@ -8,7 +8,7 @@ from .mcp.config import MCPConfig, MCPServerConfig
8
8
  from .mcp.manager import MCPManager
9
9
  from .session.session import Session
10
10
 
11
- __version__ = "1.5.5.9"
11
+ __version__ = "1.5.6.1"
12
12
 
13
13
  __all__ = [
14
14
  "Client",