python-agent-harness 1.5.5.8__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.
Files changed (121) hide show
  1. {python_agent_harness-1.5.5.8/python_agent_harness.egg-info → python_agent_harness-1.5.6.0}/PKG-INFO +102 -32
  2. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/README.md +100 -30
  3. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/pyproject.toml +7 -7
  4. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/__init__.py +7 -4
  5. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/__main__.py +1 -1
  6. python_agent_harness-1.5.6.0/python_agent_harness/core/__init__.py +1 -0
  7. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/core}/agent.py +95 -8
  8. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/core}/context_manager.py +3 -3
  9. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/core}/models.py +75 -3
  10. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/core}/planmode.py +1 -1
  11. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/core}/subagent.py +15 -5
  12. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/core}/token_estimator.py +34 -2
  13. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/core}/tool_runner.py +2 -2
  14. python_agent_harness-1.5.6.0/python_agent_harness/entry/__init__.py +1 -0
  15. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/entry}/cli.py +200 -21
  16. python_agent_harness-1.5.6.0/python_agent_harness/entry/controller.py +379 -0
  17. python_agent_harness-1.5.6.0/python_agent_harness/entry/headless.py +508 -0
  18. python_agent_harness-1.5.6.0/python_agent_harness/entry/server.py +534 -0
  19. python_agent_harness-1.5.6.0/python_agent_harness/entry/view.py +51 -0
  20. python_agent_harness-1.5.6.0/python_agent_harness/io/__init__.py +1 -0
  21. python_agent_harness-1.5.6.0/python_agent_harness/io/attachments.py +377 -0
  22. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/io}/persistence.py +98 -1
  23. python_agent_harness-1.5.6.0/python_agent_harness/io/text_filter.py +84 -0
  24. python_agent_harness-1.5.6.0/python_agent_harness/llm/__init__.py +1 -0
  25. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/llm}/client.py +105 -17
  26. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/lsp/manager.py +1 -1
  27. python_agent_harness-1.5.6.0/python_agent_harness/prompts/__init__.py +41 -0
  28. python_agent_harness-1.5.6.0/python_agent_harness/prompts/agent.md +113 -0
  29. python_agent_harness-1.5.5.8/python_agent_harness/prompts.py → python_agent_harness-1.5.6.0/python_agent_harness/prompts/core.py +108 -10
  30. python_agent_harness-1.5.6.0/python_agent_harness/prompts/subagent.md +55 -0
  31. python_agent_harness-1.5.6.0/python_agent_harness/session/__init__.py +33 -0
  32. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/session}/commands.py +3 -3
  33. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/session}/config.py +51 -19
  34. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/session}/session.py +107 -53
  35. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/__init__.py +4 -16
  36. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/agent_tool.py +45 -7
  37. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/base.py +23 -5
  38. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/bash.py +35 -5
  39. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/diffapply.py +9 -3
  40. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/edit.py +25 -4
  41. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/edit_mac.py +1 -1
  42. python_agent_harness-1.5.6.0/python_agent_harness/tools/filesystem.py +367 -0
  43. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/glob.py +21 -0
  44. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/glob_mac.py +1 -0
  45. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/grep.py +22 -1
  46. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/grep_mac.py +1 -1
  47. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/insert.py +21 -4
  48. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/lsp.py +23 -2
  49. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/mkdir.py +1 -1
  50. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/question.py +19 -1
  51. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/read.py +21 -1
  52. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/skill.py +1 -0
  53. python_agent_harness-1.5.6.0/python_agent_harness/tools/todo.py +110 -0
  54. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/write.py +33 -9
  55. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tui/__init__.py +12 -4
  56. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tui/commands.py +140 -146
  57. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tui/core.py +95 -83
  58. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tui/input.py +102 -39
  59. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tui/render.py +64 -94
  60. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0/python_agent_harness.egg-info}/PKG-INFO +102 -32
  61. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness.egg-info/SOURCES.txt +28 -42
  62. python_agent_harness-1.5.6.0/python_agent_harness.egg-info/entry_points.txt +2 -0
  63. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness.egg-info/requires.txt +1 -0
  64. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/tests/test_invariants.py +19 -14
  65. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/tests/test_scenarios.py +20 -19
  66. python_agent_harness-1.5.5.8/python_agent_harness/prompts/agent.md +0 -385
  67. python_agent_harness-1.5.5.8/python_agent_harness/prompts/subagent.md +0 -231
  68. python_agent_harness-1.5.5.8/python_agent_harness/tools/bash_win.py +0 -232
  69. python_agent_harness-1.5.5.8/python_agent_harness/tools/edit_win.py +0 -56
  70. python_agent_harness-1.5.5.8/python_agent_harness/tools/filesystem.py +0 -190
  71. python_agent_harness-1.5.5.8/python_agent_harness/tools/glob_win.py +0 -90
  72. python_agent_harness-1.5.5.8/python_agent_harness/tools/grep_win.py +0 -141
  73. python_agent_harness-1.5.5.8/python_agent_harness/tools/todo.py +0 -60
  74. python_agent_harness-1.5.5.8/python_agent_harness.egg-info/entry_points.txt +0 -2
  75. python_agent_harness-1.5.5.8/tests/test_agents_md.py +0 -260
  76. python_agent_harness-1.5.5.8/tests/test_cli.py +0 -483
  77. python_agent_harness-1.5.5.8/tests/test_client.py +0 -1952
  78. python_agent_harness-1.5.5.8/tests/test_commands.py +0 -119
  79. python_agent_harness-1.5.5.8/tests/test_concurrent_subagents.py +0 -384
  80. python_agent_harness-1.5.5.8/tests/test_config.py +0 -745
  81. python_agent_harness-1.5.5.8/tests/test_context_rules.py +0 -133
  82. python_agent_harness-1.5.5.8/tests/test_custom_agents.py +0 -629
  83. python_agent_harness-1.5.5.8/tests/test_diffrender.py +0 -68
  84. python_agent_harness-1.5.5.8/tests/test_filesystem.py +0 -2179
  85. python_agent_harness-1.5.5.8/tests/test_lsp.py +0 -984
  86. python_agent_harness-1.5.5.8/tests/test_mcp.py +0 -826
  87. python_agent_harness-1.5.5.8/tests/test_models.py +0 -130
  88. python_agent_harness-1.5.5.8/tests/test_persistence.py +0 -357
  89. python_agent_harness-1.5.5.8/tests/test_planmode.py +0 -243
  90. python_agent_harness-1.5.5.8/tests/test_prompts.py +0 -444
  91. python_agent_harness-1.5.5.8/tests/test_session.py +0 -1153
  92. python_agent_harness-1.5.5.8/tests/test_subagent.py +0 -175
  93. python_agent_harness-1.5.5.8/tests/test_subagent_isolation.py +0 -459
  94. python_agent_harness-1.5.5.8/tests/test_todos_scope.py +0 -117
  95. python_agent_harness-1.5.5.8/tests/test_token_estimator.py +0 -148
  96. python_agent_harness-1.5.5.8/tests/test_tools_misc.py +0 -563
  97. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/LICENSE +0 -0
  98. {python_agent_harness-1.5.5.8/python_agent_harness → python_agent_harness-1.5.6.0/python_agent_harness/io}/diffrender.py +0 -0
  99. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/lsp/__init__.py +0 -0
  100. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/lsp/client.py +0 -0
  101. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/lsp/config.py +0 -0
  102. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/mcp/__init__.py +0 -0
  103. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/mcp/client.py +0 -0
  104. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/mcp/config.py +0 -0
  105. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/mcp/manager.py +0 -0
  106. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/agents/reviewer.md +0 -0
  107. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/build-switch.md +0 -0
  108. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/commands/explain.md +0 -0
  109. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/compact.md +0 -0
  110. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/initialize.md +0 -0
  111. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/plan-mode.md +0 -0
  112. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/plan.md +0 -0
  113. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/review.md +0 -0
  114. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/summary.md +0 -0
  115. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/task-completion-rules.md +0 -0
  116. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/prompts/title.md +0 -0
  117. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/mcp.py +0 -0
  118. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness/tools/planexit.py +0 -0
  119. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness.egg-info/dependency_links.txt +0 -0
  120. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/python_agent_harness.egg-info/top_level.txt +0 -0
  121. {python_agent_harness-1.5.5.8 → python_agent_harness-1.5.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-agent-harness
3
- Version: 1.5.5.8
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"
@@ -105,6 +105,7 @@ Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_
105
105
 
106
106
  - **FSM-driven execution** — explicit `WAIT` / `TOOL` / `TRET` / `SUPERVISE` / `DONE` / `ERRS` / `ABRT` states. Completion supervision nudges the model when it stops early, while failed tool calls are sanitized so they never strand the agent. Transient API failures (`429` / `5xx`) retry with exponential backoff and jitter. Auth-expired status codes (configurable via `AUTH_REFRESH_STATUS_CODES` in `config.py`; defaults to `[401, 502]`) trigger automatic API key re-read from config/env — some API gateways return `502` instead of `401` when the backend auth token has expired. Note: codes in this list are treated as auth-expired exclusively and will not be retried with backoff, so only include codes that are unambiguously auth-related in your environment.
107
107
  - **Context management** — CJK-aware token estimation, per-model context windows, and automatic compaction at 70% usage.
108
+ - **Image & text attachments** — attach an image or a text file with `@path` in your message (e.g. `@screenshot.png`, `@README.md`): images (PNG/JPEG/GIF/WebP, up to 20 MB, magic-byte validated) become multimodal `image_url` parts, text files are inlined as text. Only images and text files are supported as input. Path supports web-url for image as well.
108
109
  - **Coding tools** — `Agent`, `TodoWrite`, `Glob`, `Grep`, `Read`, `Insert`, `Edit` (including unified diffs), `Write`, `Mkdir`, `Bash`, `Skill`, `Question`, `LSP`, and `PlanExit`. Synchronous tools execute sequentially, but a round made up entirely of read-only tools (`Read`, `Glob`, `Grep`, `Skill`, `LSP`) is dispatched concurrently via a bounded thread pool; asynchronous tools such as `Bash` and `Agent` can run concurrently as well. Results are always delivered in the model's emitted order.
109
110
  - **Plan / Build modes** — plan mode is read-only except for the per-session plan file.
110
111
  - **Persistent sessions** — sessions are automatically saved after every response to `~/.local/share/python-agent-harness/sessions/`, with LLM-generated titles and support for `/restore --latest` and `/sessions`.
@@ -112,6 +113,7 @@ Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_
112
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.
113
114
  - **Slash commands** — built-in `/init`, `/review`, `/explain`, and other commands, plus custom commands loaded from `prompts/commands/*.md`.
114
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).
115
117
 
116
118
  ## Inspired by opencode
117
119
 
@@ -145,7 +147,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
145
147
  "api_key": "sk-...",
146
148
  "model": "gpt-5-mini",
147
149
  "reasoning_effort": null,
148
- "stream": true
150
+ "stream": true,
151
+ "supports_image_input": false
149
152
  },
150
153
  "models": {
151
154
  "_comment": "Named LLM profiles for /model switching. Partial settings; unset keys inherit the main llm.",
@@ -171,7 +174,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
171
174
  "max_tokens": null,
172
175
  "timeout": null,
173
176
  "reasoning_effort": null,
174
- "stream": null
177
+ "stream": null,
178
+ "supports_image_input": null
175
179
  },
176
180
  "default_agent": null,
177
181
  "paths": {
@@ -210,7 +214,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
210
214
 
211
215
  ### Configuration options
212
216
 
213
- - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
217
+ - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, `stream`, and `supports_image_input`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`. `supports_image_input` (default `false`) controls whether `@path` image attachments are sent to the model or stripped with a warning.
214
218
  - **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`. `default` restores the main LLM configuration.
215
219
  - **`context_windows`** — optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g., `deepseek-v4`); matched in file order, first match wins. Overrides the built-in `CONTEXT_WINDOWS` table in `config.py`.
216
220
  - **`subagent_llm`** — LLM configuration for `Agent` tool requests. Unset values inherit from the main `llm`. Set `profile` to reuse a profile from `models`. Precedence is: profile settings > explicit `subagent_llm` settings > main `llm` > environment variables.
@@ -225,10 +229,100 @@ All LLM settings live in a single JSON configuration file. Environment variables
225
229
  ## Usage
226
230
 
227
231
  ```sh
228
- python-agent-harness run [project-dir]
232
+ python-agent-harness run [--project DIR]
229
233
  ```
230
234
 
231
- Launches the interactive TUI agent. If `project-dir` is omitted, the current directory is used.
235
+ Launches the interactive TUI agent. If `--project` is omitted, the current directory is used.
236
+
237
+ ### Headless mode
238
+
239
+ ```sh
240
+ python-agent-harness headless [prompt] [--project DIR] [--restore [SPEC]] [--model NAME]
241
+ ```
242
+
243
+ Runs a single prompt without the TUI — for CI, scripting, and piping.
244
+ The assistant's answer is written to stdout once the run completes;
245
+ tool/status events go to stderr, so the answer stream stays clean.
246
+ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
247
+ "Unanswered"), so a run never blocks.
248
+
249
+ - The prompt is a positional argument; when omitted it is read from stdin:
250
+
251
+ ```sh
252
+ python-agent-harness headless "fix the failing test"
253
+ echo "fix the failing test" | python-agent-harness headless
254
+ ```
255
+
256
+ - `--restore` continues a saved session (the conversation auto-saves after
257
+ each response). Bare `--restore` uses the most recent session; a SPEC may
258
+ be a file path or a title substring:
259
+
260
+ ```sh
261
+ python-agent-harness headless "now add tests" --restore
262
+ python-agent-harness headless "next step" --restore parser-refactor
263
+ ```
264
+
265
+ - `--model NAME` selects the model: a profile from the `models` config
266
+ section (same as the TUI's `/model`), or a raw model name on the
267
+ configured endpoint when no such profile exists. An explicit `--model`
268
+ wins over the model restored by `--restore`.
269
+
270
+ - `--json` emits the run as JSON lines on stdout instead of plain text —
271
+ one `{"type": ...}` object per line: `start` (echoes the prompt and
272
+ submit warnings), `delta` (streamed text chunks), `notify` (tool and
273
+ status events, with `kind`/`data`), `log`, and a final `result` (the
274
+ filtered answer plus any `errors`). Diagnostics (restore/model notes,
275
+ a plain-text echo of error events) still go to stderr, so one pipe
276
+ carries the structured stream. Exit codes are unchanged.
277
+
278
+ ```sh
279
+ python-agent-harness headless "fix it" --json | jq -c 'select(.type=="result")'
280
+ ```
281
+
282
+ - Exit code is 0 on success, 1 when the prompt was empty (only failed
283
+ `@file` references), the run raised an agent error, or the restore failed.
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.
232
326
 
233
327
  ### Slash commands
234
328
 
@@ -282,33 +376,9 @@ You are a personal assistant. You do NOT modify files or run shell commands.
282
376
  | **Tools** | Can restrict (`allow_planexit=False`) | All tools, minus the agent's `exclude_tools` |
283
377
  | **Ctrl-C** | Restores to default prompt | Stays on the custom agent |
284
378
 
285
- ## Project layout
286
-
287
- ```text
288
- python_agent_harness/
289
- ├── agent.py # Agent FSM core: states, transitions, supervision
290
- ├── tool_runner.py # Tool-call execution/delivery + history salvage
291
- ├── context_manager.py # Context-ratio tracking + compaction
292
- ├── client.py # OpenAI-compatible streaming client (httpx)
293
- ├── models.py # Message / ToolCall / ToolSpec data classes
294
- ├── token_estimator.py # CJK-aware token estimation + calibration
295
- ├── planmode.py # Plan/build modes + plan-file lifecycle
296
- ├── prompts.py # Prompt loading + system-prompt assembly
297
- ├── persistence.py # Session persistence + titles
298
- ├── session.py # Session wiring hub + MCP lifecycle
299
- ├── subagent.py # Sub-agent runner + error containment
300
- ├── commands.py # Init/review/custom command definitions
301
- ├── cli.py # CLI entry points
302
- ├── tui/ # Rich + prompt_toolkit TUI (package)
303
- ├── diffrender.py # Unified diff generation + Rich rendering
304
- ├── lsp/ # Built-in LSP client
305
- ├── mcp/ # Optional MCP client
306
- └── tools/ # Tool implementations + registry
307
- ```
308
-
309
379
  ## Development
310
380
 
311
- Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux, MacOS and Windows.
381
+ Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and MacOS.
312
382
 
313
383
  ```sh
314
384
  make test # unit tests
@@ -71,6 +71,7 @@ Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_
71
71
 
72
72
  - **FSM-driven execution** — explicit `WAIT` / `TOOL` / `TRET` / `SUPERVISE` / `DONE` / `ERRS` / `ABRT` states. Completion supervision nudges the model when it stops early, while failed tool calls are sanitized so they never strand the agent. Transient API failures (`429` / `5xx`) retry with exponential backoff and jitter. Auth-expired status codes (configurable via `AUTH_REFRESH_STATUS_CODES` in `config.py`; defaults to `[401, 502]`) trigger automatic API key re-read from config/env — some API gateways return `502` instead of `401` when the backend auth token has expired. Note: codes in this list are treated as auth-expired exclusively and will not be retried with backoff, so only include codes that are unambiguously auth-related in your environment.
73
73
  - **Context management** — CJK-aware token estimation, per-model context windows, and automatic compaction at 70% usage.
74
+ - **Image & text attachments** — attach an image or a text file with `@path` in your message (e.g. `@screenshot.png`, `@README.md`): images (PNG/JPEG/GIF/WebP, up to 20 MB, magic-byte validated) become multimodal `image_url` parts, text files are inlined as text. Only images and text files are supported as input. Path supports web-url for image as well.
74
75
  - **Coding tools** — `Agent`, `TodoWrite`, `Glob`, `Grep`, `Read`, `Insert`, `Edit` (including unified diffs), `Write`, `Mkdir`, `Bash`, `Skill`, `Question`, `LSP`, and `PlanExit`. Synchronous tools execute sequentially, but a round made up entirely of read-only tools (`Read`, `Glob`, `Grep`, `Skill`, `LSP`) is dispatched concurrently via a bounded thread pool; asynchronous tools such as `Bash` and `Agent` can run concurrently as well. Results are always delivered in the model's emitted order.
75
76
  - **Plan / Build modes** — plan mode is read-only except for the per-session plan file.
76
77
  - **Persistent sessions** — sessions are automatically saved after every response to `~/.local/share/python-agent-harness/sessions/`, with LLM-generated titles and support for `/restore --latest` and `/sessions`.
@@ -78,6 +79,7 @@ Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_
78
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.
79
80
  - **Slash commands** — built-in `/init`, `/review`, `/explain`, and other commands, plus custom commands loaded from `prompts/commands/*.md`.
80
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).
81
83
 
82
84
  ## Inspired by opencode
83
85
 
@@ -111,7 +113,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
111
113
  "api_key": "sk-...",
112
114
  "model": "gpt-5-mini",
113
115
  "reasoning_effort": null,
114
- "stream": true
116
+ "stream": true,
117
+ "supports_image_input": false
115
118
  },
116
119
  "models": {
117
120
  "_comment": "Named LLM profiles for /model switching. Partial settings; unset keys inherit the main llm.",
@@ -137,7 +140,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
137
140
  "max_tokens": null,
138
141
  "timeout": null,
139
142
  "reasoning_effort": null,
140
- "stream": null
143
+ "stream": null,
144
+ "supports_image_input": null
141
145
  },
142
146
  "default_agent": null,
143
147
  "paths": {
@@ -176,7 +180,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
176
180
 
177
181
  ### Configuration options
178
182
 
179
- - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
183
+ - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, `stream`, and `supports_image_input`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`. `supports_image_input` (default `false`) controls whether `@path` image attachments are sent to the model or stripped with a warning.
180
184
  - **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`. `default` restores the main LLM configuration.
181
185
  - **`context_windows`** — optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g., `deepseek-v4`); matched in file order, first match wins. Overrides the built-in `CONTEXT_WINDOWS` table in `config.py`.
182
186
  - **`subagent_llm`** — LLM configuration for `Agent` tool requests. Unset values inherit from the main `llm`. Set `profile` to reuse a profile from `models`. Precedence is: profile settings > explicit `subagent_llm` settings > main `llm` > environment variables.
@@ -191,10 +195,100 @@ All LLM settings live in a single JSON configuration file. Environment variables
191
195
  ## Usage
192
196
 
193
197
  ```sh
194
- python-agent-harness run [project-dir]
198
+ python-agent-harness run [--project DIR]
195
199
  ```
196
200
 
197
- Launches the interactive TUI agent. If `project-dir` is omitted, the current directory is used.
201
+ Launches the interactive TUI agent. If `--project` is omitted, the current directory is used.
202
+
203
+ ### Headless mode
204
+
205
+ ```sh
206
+ python-agent-harness headless [prompt] [--project DIR] [--restore [SPEC]] [--model NAME]
207
+ ```
208
+
209
+ Runs a single prompt without the TUI — for CI, scripting, and piping.
210
+ The assistant's answer is written to stdout once the run completes;
211
+ tool/status events go to stderr, so the answer stream stays clean.
212
+ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
213
+ "Unanswered"), so a run never blocks.
214
+
215
+ - The prompt is a positional argument; when omitted it is read from stdin:
216
+
217
+ ```sh
218
+ python-agent-harness headless "fix the failing test"
219
+ echo "fix the failing test" | python-agent-harness headless
220
+ ```
221
+
222
+ - `--restore` continues a saved session (the conversation auto-saves after
223
+ each response). Bare `--restore` uses the most recent session; a SPEC may
224
+ be a file path or a title substring:
225
+
226
+ ```sh
227
+ python-agent-harness headless "now add tests" --restore
228
+ python-agent-harness headless "next step" --restore parser-refactor
229
+ ```
230
+
231
+ - `--model NAME` selects the model: a profile from the `models` config
232
+ section (same as the TUI's `/model`), or a raw model name on the
233
+ configured endpoint when no such profile exists. An explicit `--model`
234
+ wins over the model restored by `--restore`.
235
+
236
+ - `--json` emits the run as JSON lines on stdout instead of plain text —
237
+ one `{"type": ...}` object per line: `start` (echoes the prompt and
238
+ submit warnings), `delta` (streamed text chunks), `notify` (tool and
239
+ status events, with `kind`/`data`), `log`, and a final `result` (the
240
+ filtered answer plus any `errors`). Diagnostics (restore/model notes,
241
+ a plain-text echo of error events) still go to stderr, so one pipe
242
+ carries the structured stream. Exit codes are unchanged.
243
+
244
+ ```sh
245
+ python-agent-harness headless "fix it" --json | jq -c 'select(.type=="result")'
246
+ ```
247
+
248
+ - Exit code is 0 on success, 1 when the prompt was empty (only failed
249
+ `@file` references), the run raised an agent error, or the restore failed.
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.
198
292
 
199
293
  ### Slash commands
200
294
 
@@ -248,33 +342,9 @@ You are a personal assistant. You do NOT modify files or run shell commands.
248
342
  | **Tools** | Can restrict (`allow_planexit=False`) | All tools, minus the agent's `exclude_tools` |
249
343
  | **Ctrl-C** | Restores to default prompt | Stays on the custom agent |
250
344
 
251
- ## Project layout
252
-
253
- ```text
254
- python_agent_harness/
255
- ├── agent.py # Agent FSM core: states, transitions, supervision
256
- ├── tool_runner.py # Tool-call execution/delivery + history salvage
257
- ├── context_manager.py # Context-ratio tracking + compaction
258
- ├── client.py # OpenAI-compatible streaming client (httpx)
259
- ├── models.py # Message / ToolCall / ToolSpec data classes
260
- ├── token_estimator.py # CJK-aware token estimation + calibration
261
- ├── planmode.py # Plan/build modes + plan-file lifecycle
262
- ├── prompts.py # Prompt loading + system-prompt assembly
263
- ├── persistence.py # Session persistence + titles
264
- ├── session.py # Session wiring hub + MCP lifecycle
265
- ├── subagent.py # Sub-agent runner + error containment
266
- ├── commands.py # Init/review/custom command definitions
267
- ├── cli.py # CLI entry points
268
- ├── tui/ # Rich + prompt_toolkit TUI (package)
269
- ├── diffrender.py # Unified diff generation + Rich rendering
270
- ├── lsp/ # Built-in LSP client
271
- ├── mcp/ # Optional MCP client
272
- └── tools/ # Tool implementations + registry
273
- ```
274
-
275
345
  ## Development
276
346
 
277
- Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux, MacOS and Windows.
347
+ Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and MacOS.
278
348
 
279
349
  ```sh
280
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.5.8"
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
 
@@ -57,7 +57,7 @@ dev = [
57
57
  ]
58
58
 
59
59
  [project.scripts]
60
- python-agent-harness = "python_agent_harness.cli:main"
60
+ python-agent-harness = "python_agent_harness.entry.cli:main"
61
61
 
62
62
  [tool.setuptools.packages.find]
63
63
  include = ["python_agent_harness*"]
@@ -90,10 +90,10 @@ source = ["python_agent_harness"]
90
90
  branch = true
91
91
  # The console/TUI entry points aren't exercised by unittest; omit the ones
92
92
  # that would otherwise drag the number down without adding signal.
93
- omit = ["python_agent_harness/__main__.py"]
93
+ omit = [
94
+ "python_agent_harness/__main__.py",
95
+ ]
94
96
 
95
97
  [tool.coverage.report]
96
98
  show_missing = true
97
- # Current line + branch coverage is ~98%. Gate at 90 to leave headroom for
98
- # small changes while still catching real regressions. Raise as coverage grows.
99
- fail_under = 90
99
+ fail_under = 95
@@ -1,13 +1,14 @@
1
1
  """python-agent-harness: a Python port of the gptel-agent-harness."""
2
2
 
3
- from .client import Client, LLMClient
3
+ from .core.models import AgentMode, ImagePart, Message, TextPart, ToolCall, ToolSpec
4
+ from .entry.cli import main # noqa: F401 (public lib surface: main entry point)
5
+ from .llm.client import Client, LLMClient
4
6
  from .lsp.config import LSPConfig, LSPServerConfig
5
7
  from .mcp.config import MCPConfig, MCPServerConfig
6
8
  from .mcp.manager import MCPManager
7
- from .models import AgentMode, Message, ToolCall, ToolSpec
8
- from .session import Session
9
+ from .session.session import Session
9
10
 
10
- __version__ = "1.5.5.8"
11
+ __version__ = "1.5.6.0"
11
12
 
12
13
  __all__ = [
13
14
  "Client",
@@ -20,6 +21,8 @@ __all__ = [
20
21
  "MCPManager",
21
22
  "MCPServerConfig",
22
23
  "Message",
24
+ "TextPart",
25
+ "ImagePart",
23
26
  "ToolCall",
24
27
  "ToolSpec",
25
28
  "__version__",
@@ -1,5 +1,5 @@
1
1
  import sys
2
2
 
3
- from .cli import main
3
+ from .entry.cli import main
4
4
 
5
5
  sys.exit(main())
@@ -0,0 +1 @@
1
+ """Core engine: agent loop, tool dispatch, models, and context utilities."""