python-agent-harness 1.5.5.4__tar.gz → 1.5.5.6__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 (104) hide show
  1. {python_agent_harness-1.5.5.4/python_agent_harness.egg-info → python_agent_harness-1.5.5.6}/PKG-INFO +34 -3
  2. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/README.md +33 -2
  3. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/pyproject.toml +1 -1
  4. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/__init__.py +7 -1
  5. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/agent.py +3 -2
  6. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/cli.py +13 -0
  7. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/client.py +70 -1
  8. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/config.py +36 -0
  9. python_agent_harness-1.5.5.6/python_agent_harness/lsp/__init__.py +14 -0
  10. python_agent_harness-1.5.5.6/python_agent_harness/lsp/client.py +314 -0
  11. python_agent_harness-1.5.5.6/python_agent_harness/lsp/config.py +108 -0
  12. python_agent_harness-1.5.5.6/python_agent_harness/lsp/manager.py +117 -0
  13. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/agent.md +24 -1
  14. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/agents/reviewer.md +10 -0
  15. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/subagent.md +24 -1
  16. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts.py +62 -14
  17. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/session.py +72 -22
  18. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/subagent.py +2 -1
  19. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/__init__.py +3 -0
  20. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/base.py +82 -10
  21. python_agent_harness-1.5.5.6/python_agent_harness/tools/lsp.py +243 -0
  22. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tui/commands.py +3 -0
  23. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6/python_agent_harness.egg-info}/PKG-INFO +34 -3
  24. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness.egg-info/SOURCES.txt +6 -0
  25. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_cli.py +22 -1
  26. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_config.py +124 -0
  27. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_custom_agents.py +223 -0
  28. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_filesystem.py +39 -2
  29. python_agent_harness-1.5.5.6/tests/test_lsp.py +984 -0
  30. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_tools_misc.py +50 -1
  31. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/LICENSE +0 -0
  32. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/__main__.py +0 -0
  33. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/commands.py +0 -0
  34. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/context_manager.py +0 -0
  35. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/diffrender.py +0 -0
  36. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/mcp/__init__.py +0 -0
  37. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/mcp/client.py +0 -0
  38. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/mcp/config.py +0 -0
  39. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/mcp/manager.py +0 -0
  40. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/models.py +0 -0
  41. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/persistence.py +0 -0
  42. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/planmode.py +0 -0
  43. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/build-switch.md +0 -0
  44. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/commands/explain.md +0 -0
  45. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/compact.md +0 -0
  46. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/initialize.md +0 -0
  47. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/plan-mode.md +0 -0
  48. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/plan.md +0 -0
  49. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/review.md +0 -0
  50. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/summary.md +0 -0
  51. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/task-completion-rules.md +0 -0
  52. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/title.md +0 -0
  53. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/token_estimator.py +0 -0
  54. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tool_runner.py +0 -0
  55. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/agent_tool.py +0 -0
  56. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/bash.py +0 -0
  57. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/bash_win.py +0 -0
  58. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/diffapply.py +0 -0
  59. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/edit.py +0 -0
  60. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/edit_mac.py +0 -0
  61. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/edit_win.py +0 -0
  62. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/filesystem.py +0 -0
  63. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/glob.py +0 -0
  64. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/glob_mac.py +0 -0
  65. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/glob_win.py +0 -0
  66. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/grep.py +0 -0
  67. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/grep_mac.py +0 -0
  68. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/grep_win.py +0 -0
  69. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/insert.py +0 -0
  70. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/mcp.py +0 -0
  71. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/mkdir.py +0 -0
  72. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/planexit.py +0 -0
  73. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/question.py +0 -0
  74. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/read.py +0 -0
  75. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/skill.py +0 -0
  76. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/todo.py +0 -0
  77. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/write.py +0 -0
  78. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tui/__init__.py +0 -0
  79. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tui/core.py +0 -0
  80. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tui/input.py +0 -0
  81. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness/tui/render.py +0 -0
  82. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness.egg-info/dependency_links.txt +0 -0
  83. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness.egg-info/entry_points.txt +0 -0
  84. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness.egg-info/requires.txt +0 -0
  85. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/python_agent_harness.egg-info/top_level.txt +0 -0
  86. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/setup.cfg +0 -0
  87. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_agents_md.py +0 -0
  88. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_client.py +0 -0
  89. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_commands.py +0 -0
  90. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_concurrent_subagents.py +0 -0
  91. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_context_rules.py +0 -0
  92. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_diffrender.py +0 -0
  93. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_invariants.py +0 -0
  94. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_mcp.py +0 -0
  95. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_models.py +0 -0
  96. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_persistence.py +0 -0
  97. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_planmode.py +0 -0
  98. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_prompts.py +0 -0
  99. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_scenarios.py +0 -0
  100. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_session.py +0 -0
  101. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_subagent.py +0 -0
  102. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_subagent_isolation.py +0 -0
  103. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_todos_scope.py +0 -0
  104. {python_agent_harness-1.5.5.4 → python_agent_harness-1.5.5.6}/tests/test_token_estimator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-agent-harness
3
- Version: 1.5.5.4
3
+ Version: 1.5.5.6
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
@@ -105,7 +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
- - **Coding tools** — `Agent`, `TodoWrite`, `Glob`, `Grep`, `Read`, `Insert`, `Edit` (including unified diffs), `Write`, `Mkdir`, `Bash`, `Skill`, `Question`, and `PlanExit`. Synchronous tools execute sequentially, but a round made up entirely of read-only tools (`Read`, `Glob`, `Grep`, `Skill`) 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.
108
+ - **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
109
  - **Plan / Build modes** — plan mode is read-only except for the per-session plan file.
110
110
  - **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`.
111
111
  - **Focused TUI** — a Rich-based interface with a pinned status bar, Todos panel, inline red/green diff rendering for `Edit` and `Write`, and a `prompt_toolkit` editor with history and completion. `Esc+Enter` submits, `Ctrl-D` quits, and `Ctrl-C` cancels without leaving the application.
@@ -178,6 +178,16 @@ All LLM settings live in a single JSON configuration file. Environment variables
178
178
  "context_path": null,
179
179
  "skill_path": null
180
180
  },
181
+ "lsp": {
182
+ "servers": {
183
+ ".c": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "c" },
184
+ ".h": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "c" },
185
+ ".cpp": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "cpp" },
186
+ ".cc": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "cpp" },
187
+ ".hpp": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "cpp" },
188
+ ".cxx": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "cpp" }
189
+ }
190
+ },
181
191
  "mcp": {
182
192
  "servers": {
183
193
  "example": {
@@ -206,6 +216,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
206
216
  - **`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.
207
217
  - **`default_agent`** — name of the agent to use at session start (instead of the built-in `agent.md`). The agent must exist as a `.md` file in the `prompts/agents/` directory. When unset or `null`, the built-in default agent is used. Use `/agent default` in the TUI to switch back to the built-in at any time.
208
218
  - **`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.
219
+ - **`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`.
209
220
  - **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
210
221
  - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
211
222
  - **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
@@ -243,13 +254,32 @@ Custom commands from `prompts/commands/*.md` are registered as slash commands as
243
254
 
244
255
  Agent prompt files are markdown files (`.md`) placed in the `prompts/agents/` directory. Each file becomes a switchable agent profile available via the `/agent` TUI command.
245
256
 
257
+ An agent file may carry YAML frontmatter with two optional keys:
258
+
259
+ - `name:` — override the agent name (defaults to the file stem)
260
+ - `exclude_tools:` — a list of tool names the agent must not see. An entry matches a tool by exact name, glob pattern (`mcp__git__*`), or `__`-delimited prefix (`mcp__git` hides `mcp__git__list_repos` but `Write` does NOT hide `TodoWrite`). The built-in `default` agent always sees all tools.
261
+
262
+ ```markdown
263
+ ---
264
+ name: assistant
265
+ exclude_tools:
266
+ - Bash
267
+ - Edit
268
+ - Write
269
+ - mcp__git__*
270
+ ---
271
+
272
+ # Role and Behavior
273
+ You are a personal assistant. You do NOT modify files or run shell commands.
274
+ ```
275
+
246
276
  #### Commands vs Agents
247
277
 
248
278
  | | Commands (`/review`) | Agents (`/agent reviewer`) |
249
279
  |---|---|---|
250
280
  | **Scope** | One-shot (prompt resets after run) | Persistent (stays until next `/agent`) |
251
281
  | **Kickoff** | Hardcoded kickoff message | User types their own prompt |
252
- | **Tools** | Can restrict (`allow_planexit=False`) | All tools |
282
+ | **Tools** | Can restrict (`allow_planexit=False`) | All tools, minus the agent's `exclude_tools` |
253
283
  | **Ctrl-C** | Restores to default prompt | Stays on the custom agent |
254
284
 
255
285
  ## Project layout
@@ -271,6 +301,7 @@ python_agent_harness/
271
301
  ├── cli.py # CLI entry points
272
302
  ├── tui/ # Rich + prompt_toolkit TUI (package)
273
303
  ├── diffrender.py # Unified diff generation + Rich rendering
304
+ ├── lsp/ # Built-in LSP client
274
305
  ├── mcp/ # Optional MCP client
275
306
  └── tools/ # Tool implementations + registry
276
307
  ```
@@ -71,7 +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
- - **Coding tools** — `Agent`, `TodoWrite`, `Glob`, `Grep`, `Read`, `Insert`, `Edit` (including unified diffs), `Write`, `Mkdir`, `Bash`, `Skill`, `Question`, and `PlanExit`. Synchronous tools execute sequentially, but a round made up entirely of read-only tools (`Read`, `Glob`, `Grep`, `Skill`) 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.
74
+ - **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
75
  - **Plan / Build modes** — plan mode is read-only except for the per-session plan file.
76
76
  - **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`.
77
77
  - **Focused TUI** — a Rich-based interface with a pinned status bar, Todos panel, inline red/green diff rendering for `Edit` and `Write`, and a `prompt_toolkit` editor with history and completion. `Esc+Enter` submits, `Ctrl-D` quits, and `Ctrl-C` cancels without leaving the application.
@@ -144,6 +144,16 @@ All LLM settings live in a single JSON configuration file. Environment variables
144
144
  "context_path": null,
145
145
  "skill_path": null
146
146
  },
147
+ "lsp": {
148
+ "servers": {
149
+ ".c": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "c" },
150
+ ".h": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "c" },
151
+ ".cpp": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "cpp" },
152
+ ".cc": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "cpp" },
153
+ ".hpp": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "cpp" },
154
+ ".cxx": { "command": ["clangd", "--compile-commands-dir=$REPO/build/Linux_x86_64", "--background-index"], "language_id": "cpp" }
155
+ }
156
+ },
147
157
  "mcp": {
148
158
  "servers": {
149
159
  "example": {
@@ -172,6 +182,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
172
182
  - **`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.
173
183
  - **`default_agent`** — name of the agent to use at session start (instead of the built-in `agent.md`). The agent must exist as a `.md` file in the `prompts/agents/` directory. When unset or `null`, the built-in default agent is used. Use `/agent default` in the TUI to switch back to the built-in at any time.
174
184
  - **`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.
185
+ - **`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`.
175
186
  - **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
176
187
  - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
177
188
  - **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
@@ -209,13 +220,32 @@ Custom commands from `prompts/commands/*.md` are registered as slash commands as
209
220
 
210
221
  Agent prompt files are markdown files (`.md`) placed in the `prompts/agents/` directory. Each file becomes a switchable agent profile available via the `/agent` TUI command.
211
222
 
223
+ An agent file may carry YAML frontmatter with two optional keys:
224
+
225
+ - `name:` — override the agent name (defaults to the file stem)
226
+ - `exclude_tools:` — a list of tool names the agent must not see. An entry matches a tool by exact name, glob pattern (`mcp__git__*`), or `__`-delimited prefix (`mcp__git` hides `mcp__git__list_repos` but `Write` does NOT hide `TodoWrite`). The built-in `default` agent always sees all tools.
227
+
228
+ ```markdown
229
+ ---
230
+ name: assistant
231
+ exclude_tools:
232
+ - Bash
233
+ - Edit
234
+ - Write
235
+ - mcp__git__*
236
+ ---
237
+
238
+ # Role and Behavior
239
+ You are a personal assistant. You do NOT modify files or run shell commands.
240
+ ```
241
+
212
242
  #### Commands vs Agents
213
243
 
214
244
  | | Commands (`/review`) | Agents (`/agent reviewer`) |
215
245
  |---|---|---|
216
246
  | **Scope** | One-shot (prompt resets after run) | Persistent (stays until next `/agent`) |
217
247
  | **Kickoff** | Hardcoded kickoff message | User types their own prompt |
218
- | **Tools** | Can restrict (`allow_planexit=False`) | All tools |
248
+ | **Tools** | Can restrict (`allow_planexit=False`) | All tools, minus the agent's `exclude_tools` |
219
249
  | **Ctrl-C** | Restores to default prompt | Stays on the custom agent |
220
250
 
221
251
  ## Project layout
@@ -237,6 +267,7 @@ python_agent_harness/
237
267
  ├── cli.py # CLI entry points
238
268
  ├── tui/ # Rich + prompt_toolkit TUI (package)
239
269
  ├── diffrender.py # Unified diff generation + Rich rendering
270
+ ├── lsp/ # Built-in LSP client
240
271
  ├── mcp/ # Optional MCP client
241
272
  └── tools/ # Tool implementations + registry
242
273
  ```
@@ -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.4"
7
+ version = "1.5.5.6"
8
8
  description = "A lightweight, hackable mini-OpenCode written in Python."
9
9
  authors = [
10
10
  {name = "Huming Chen", email = "chenhuming@gmail.com"}
@@ -1,15 +1,21 @@
1
1
  """python-agent-harness: a Python port of the gptel-agent-harness."""
2
2
 
3
+ from .client import Client, LLMClient
4
+ from .lsp.config import LSPConfig, LSPServerConfig
3
5
  from .mcp.config import MCPConfig, MCPServerConfig
4
6
  from .mcp.manager import MCPManager
5
7
  from .models import AgentMode, Message, ToolCall, ToolSpec
6
8
  from .session import Session
7
9
 
8
- __version__ = "1.5.5.4"
10
+ __version__ = "1.5.5.6"
9
11
 
10
12
  __all__ = [
13
+ "Client",
14
+ "LLMClient",
11
15
  "Session",
12
16
  "AgentMode",
17
+ "LSPConfig",
18
+ "LSPServerConfig",
13
19
  "MCPConfig",
14
20
  "MCPManager",
15
21
  "MCPServerConfig",
@@ -38,6 +38,7 @@ from __future__ import annotations
38
38
  from typing import Any, Protocol
39
39
 
40
40
  from . import config
41
+ from .client import LLMClient
41
42
  from .context_manager import ContextManager
42
43
  from .models import Message, ToolCall
43
44
  from .token_estimator import context_window_for, estimate_payload_tokens
@@ -74,7 +75,7 @@ class AgentLoop:
74
75
  top_level: bool = True,
75
76
  system: str | None = None,
76
77
  max_rounds: int = 60,
77
- client: Any | None = None,
78
+ client: LLMClient | None = None,
78
79
  ) -> None:
79
80
  self.session = session
80
81
  self.messages: list[Message] = messages if messages is not None else []
@@ -631,7 +632,7 @@ def run_agent_loop(
631
632
  top_level: bool = True,
632
633
  system: str | None = None,
633
634
  max_rounds: int = 60,
634
- client: Any | None = None,
635
+ client: LLMClient | None = None,
635
636
  ) -> str | None:
636
637
  """Convenience wrapper running a full agent run (FSM).
637
638
 
@@ -41,6 +41,11 @@ def make_session(
41
41
  settings = config.load_llm_config(config_path)
42
42
  paths = config.load_paths_config(config_path)
43
43
  mcp_config = config.load_mcp_config(config_path)
44
+ # Validate lsp.servers eagerly so a malformed override fails fast at
45
+ # session start (with a clear message) instead of poisoning the first
46
+ # LSP tool call mid-session. The LSP manager re-reads it lazily via
47
+ # config_path; this call only surfaces config errors early.
48
+ config.load_lsp_config(config_path)
44
49
  model = model or settings["model"]
45
50
  # resolve sub-agent overrides against the EFFECTIVE main settings
46
51
  # (so a CLI/caller model override is inherited too when the
@@ -243,6 +248,14 @@ def cmd_config(args: argparse.Namespace) -> int:
243
248
  print(f" {pattern}: {size}")
244
249
  else:
245
250
  print("context_windows: (none configured — built-in table in config.py applies)")
251
+ # Show LSP server overrides (per-extension, layered over the built-ins)
252
+ lsp_config = config.load_lsp_config(args.path)
253
+ if lsp_config.servers:
254
+ print("lsp servers:")
255
+ for ext, server in sorted(lsp_config.servers.items()):
256
+ print(f" {ext}: language_id={server.language_id}, command={' '.join(server.command)}")
257
+ else:
258
+ print("lsp servers: (none configured — built-in DEFAULT_SERVERS in lsp/manager.py applies)")
246
259
  # Show default agent
247
260
  default_agent = config.load_default_agent(args.path)
248
261
  print(f"default_agent: {default_agent or '(default: agent.md)'}")
@@ -19,13 +19,82 @@ import time
19
19
  import uuid
20
20
  from collections.abc import Callable, Iterator
21
21
  from pathlib import Path
22
- from typing import Any
22
+ from typing import Any, Protocol, runtime_checkable
23
23
 
24
24
  import httpx
25
25
 
26
26
  from . import config
27
27
  from .models import Message, ToolCall, ToolSpec, Usage
28
28
 
29
+
30
+ @runtime_checkable
31
+ class LLMClient(Protocol):
32
+ """Structural interface the agent loop needs from an LLM backend.
33
+
34
+ ``AgentLoop`` and ``Session`` depend only on this surface, never on
35
+ the concrete ``Client`` transport. Anything providing these
36
+ members — the OpenAI-compatible ``Client`` below, a fake/replay
37
+ double in tests, or an alternative provider — can drive the FSM.
38
+
39
+ The members mirror how the agent consumes a client today:
40
+
41
+ - ``chat`` / ``chat_sync`` — the two request entry points (streaming
42
+ turns and one-shot compaction/title/summary calls).
43
+ - ``clone`` — a fresh, state-isolated copy for a concurrent
44
+ sub-agent (pools and the abort flag must never be shared).
45
+ - ``abort`` / ``close`` — cancel an in-flight request / release the
46
+ pool.
47
+ - ``set_timeout`` — apply a new request timeout (``/model`` switch).
48
+ - ``base_url`` / ``api_key`` / ``model`` / ``timeout`` — connection
49
+ settings the session reads and rewrites on a profile switch.
50
+ - ``context_window`` — the model's window (config-aware), read by
51
+ the context manager for compaction decisions.
52
+ - ``log_path`` — the shared LLM log file, inherited by clones.
53
+ """
54
+
55
+ base_url: str
56
+ api_key: str | None
57
+ model: str
58
+ timeout: float
59
+ log_path: Path | None
60
+
61
+ @property
62
+ def context_window(self) -> int: ...
63
+
64
+ def chat(
65
+ self,
66
+ messages: list[Message],
67
+ tools: list[ToolSpec] | None = ...,
68
+ system: str | None = ...,
69
+ temperature: float | None = ...,
70
+ max_tokens: int | None = ...,
71
+ reasoning_effort: str | None = ...,
72
+ on_delta: Callable[[str], None] | None = ...,
73
+ on_tool_call: Callable[[str, str, str], None] | None = ...,
74
+ stream: bool = ...,
75
+ cancel_check: Callable[[], bool] | None = ...,
76
+ on_retry: Callable[[], None] | None = ...,
77
+ ) -> tuple[Message, Usage]: ...
78
+
79
+ def chat_sync(
80
+ self,
81
+ messages: list[Message],
82
+ system: str | None = ...,
83
+ temperature: float | None = ...,
84
+ max_tokens: int | None = ...,
85
+ reasoning_effort: str | None = ...,
86
+ cancel_check: Callable[[], bool] | None = ...,
87
+ ) -> tuple[Message, Usage]: ...
88
+
89
+ def clone(self) -> LLMClient: ...
90
+
91
+ def abort(self) -> None: ...
92
+
93
+ def close(self) -> None: ...
94
+
95
+ def set_timeout(self, timeout: float) -> None: ...
96
+
97
+
29
98
  # serializes appends to the shared LLM log file: concurrent sub-agents
30
99
  # (each with its own client but ONE shared log_path, see Client.clone)
31
100
  # finish their interactions in parallel, and interleaved write() calls
@@ -7,6 +7,7 @@ import os
7
7
  import sys
8
8
  from pathlib import Path
9
9
 
10
+ from .lsp.config import LSPConfig
10
11
  from .mcp.config import MCPConfig
11
12
 
12
13
  # ---- context management -------------------------------------------------
@@ -98,6 +99,7 @@ DEFAULT_TOOLS: list[str] = [
98
99
  "Bash",
99
100
  "Skill",
100
101
  "Question",
102
+ "LSP",
101
103
  ]
102
104
 
103
105
  # ---- tool output limits --------------------------------------------------------
@@ -255,6 +257,15 @@ CONFIG_TEMPLATE = """\
255
257
  "context_path": null,
256
258
  "skill_path": null
257
259
  }},
260
+ "lsp": {{
261
+ "_comment": "Optional per-extension LSP server overrides. Keys are file extensions (e.g. '.py', '.cpp'); 'command' is the server argv; 'language_id' defaults to the extension without the dot. These override the built-in DEFAULT_SERVERS table. Remove this section to use only the built-ins. The server binary must be on PATH. The '.example' entry below is inert (no such extension) — copy it to a real extension like '.cpp' to activate.",
262
+ "servers": {{
263
+ ".example": {{
264
+ "command": ["clangd", "--background-index", "--clang-tidy"],
265
+ "language_id": "cpp"
266
+ }}
267
+ }}
268
+ }},
258
269
  "mcp": {{
259
270
  "_comment": "Optional MCP servers (requires: pip install -e '.[mcp]'). Each server's tools become agent tools named mcp__<server>__<tool>. Transports: stdio (spawn command+args, pass through env var names), streamable-http / sse (connect to url, optional headers). 'parallel: true' marks read-only servers whose tools may run concurrently; default is serial. 'timeout' bounds connects, discovery and calls (seconds).",
260
271
  "servers": {{
@@ -446,6 +457,31 @@ def load_mcp_config(path: str | os.PathLike | None = None) -> MCPConfig:
446
457
  return MCPConfig.from_dict(section.get("servers"))
447
458
 
448
459
 
460
+ def load_lsp_config(path: str | os.PathLike | None = None) -> LSPConfig:
461
+ """Load per-extension LSP server overrides from the config file.
462
+
463
+ Reads the optional ``lsp.servers`` object (keyed by file extension,
464
+ e.g. ``".py"``) and returns an ``LSPConfig`` (empty when the file
465
+ has no ``lsp`` section or no servers). These entries layer on top of
466
+ the built-in ``DEFAULT_SERVERS`` table in ``lsp/manager.py``.
467
+
468
+ Malformed entries raise ValueError so config errors surface at
469
+ session start rather than the first LSP call.
470
+ """
471
+ data = _read_config(path)
472
+ section = data.get("lsp") or {}
473
+ if not isinstance(section, dict):
474
+ raise ValueError(f"config file {_config_path(path)}: lsp must be an object")
475
+ servers = section.get("servers") or {}
476
+ if not isinstance(servers, dict):
477
+ raise ValueError(f"config file {_config_path(path)}: lsp.servers must be an object")
478
+ try:
479
+ return LSPConfig.from_dict(servers)
480
+ except ValueError as e:
481
+ # Re-raise with the config-file path for a clearer message.
482
+ raise ValueError(f"config file {_config_path(path)}: {e}") from e
483
+
484
+
449
485
  def load_models_config(path: str | os.PathLike | None = None) -> dict[str, dict]:
450
486
  """Load named LLM profiles from the config file's ``models`` object.
451
487
 
@@ -0,0 +1,14 @@
1
+ """Minimal built-in LSP client used by python-agent-harness."""
2
+
3
+ from .client import LSPClient, LSPError
4
+ from .config import LSPConfig, LSPServerConfig
5
+ from .manager import get_client, shutdown_all
6
+
7
+ __all__ = [
8
+ "LSPClient",
9
+ "LSPConfig",
10
+ "LSPError",
11
+ "LSPServerConfig",
12
+ "get_client",
13
+ "shutdown_all",
14
+ ]