python-agent-harness 1.5.5.5__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.5/python_agent_harness.egg-info → python_agent_harness-1.5.5.6}/PKG-INFO +14 -2
  2. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/README.md +13 -1
  3. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/pyproject.toml +1 -1
  4. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/__init__.py +7 -1
  5. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/agent.py +3 -2
  6. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/cli.py +13 -0
  7. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/client.py +70 -1
  8. {python_agent_harness-1.5.5.5 → 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.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/agent.md +24 -1
  14. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/subagent.md +24 -1
  15. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/session.py +27 -18
  16. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/subagent.py +2 -1
  17. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/__init__.py +3 -0
  18. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/base.py +82 -10
  19. python_agent_harness-1.5.5.6/python_agent_harness/tools/lsp.py +243 -0
  20. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6/python_agent_harness.egg-info}/PKG-INFO +14 -2
  21. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness.egg-info/SOURCES.txt +6 -0
  22. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_cli.py +22 -1
  23. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_config.py +124 -0
  24. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_filesystem.py +39 -2
  25. python_agent_harness-1.5.5.6/tests/test_lsp.py +984 -0
  26. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_tools_misc.py +50 -1
  27. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/LICENSE +0 -0
  28. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/__main__.py +0 -0
  29. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/commands.py +0 -0
  30. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/context_manager.py +0 -0
  31. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/diffrender.py +0 -0
  32. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/mcp/__init__.py +0 -0
  33. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/mcp/client.py +0 -0
  34. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/mcp/config.py +0 -0
  35. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/mcp/manager.py +0 -0
  36. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/models.py +0 -0
  37. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/persistence.py +0 -0
  38. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/planmode.py +0 -0
  39. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/agents/reviewer.md +0 -0
  40. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/build-switch.md +0 -0
  41. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/commands/explain.md +0 -0
  42. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/compact.md +0 -0
  43. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/initialize.md +0 -0
  44. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/plan-mode.md +0 -0
  45. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/plan.md +0 -0
  46. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/review.md +0 -0
  47. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/summary.md +0 -0
  48. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/task-completion-rules.md +0 -0
  49. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts/title.md +0 -0
  50. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/prompts.py +0 -0
  51. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/token_estimator.py +0 -0
  52. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tool_runner.py +0 -0
  53. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/agent_tool.py +0 -0
  54. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/bash.py +0 -0
  55. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/bash_win.py +0 -0
  56. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/diffapply.py +0 -0
  57. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/edit.py +0 -0
  58. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/edit_mac.py +0 -0
  59. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/edit_win.py +0 -0
  60. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/filesystem.py +0 -0
  61. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/glob.py +0 -0
  62. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/glob_mac.py +0 -0
  63. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/glob_win.py +0 -0
  64. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/grep.py +0 -0
  65. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/grep_mac.py +0 -0
  66. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/grep_win.py +0 -0
  67. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/insert.py +0 -0
  68. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/mcp.py +0 -0
  69. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/mkdir.py +0 -0
  70. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/planexit.py +0 -0
  71. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/question.py +0 -0
  72. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/read.py +0 -0
  73. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/skill.py +0 -0
  74. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/todo.py +0 -0
  75. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tools/write.py +0 -0
  76. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tui/__init__.py +0 -0
  77. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tui/commands.py +0 -0
  78. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tui/core.py +0 -0
  79. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tui/input.py +0 -0
  80. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness/tui/render.py +0 -0
  81. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness.egg-info/dependency_links.txt +0 -0
  82. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness.egg-info/entry_points.txt +0 -0
  83. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness.egg-info/requires.txt +0 -0
  84. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/python_agent_harness.egg-info/top_level.txt +0 -0
  85. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/setup.cfg +0 -0
  86. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_agents_md.py +0 -0
  87. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_client.py +0 -0
  88. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_commands.py +0 -0
  89. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_concurrent_subagents.py +0 -0
  90. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_context_rules.py +0 -0
  91. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_custom_agents.py +0 -0
  92. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_diffrender.py +0 -0
  93. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_invariants.py +0 -0
  94. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_mcp.py +0 -0
  95. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_models.py +0 -0
  96. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_persistence.py +0 -0
  97. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_planmode.py +0 -0
  98. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_prompts.py +0 -0
  99. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_scenarios.py +0 -0
  100. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_session.py +0 -0
  101. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_subagent.py +0 -0
  102. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_subagent_isolation.py +0 -0
  103. {python_agent_harness-1.5.5.5 → python_agent_harness-1.5.5.6}/tests/test_todos_scope.py +0 -0
  104. {python_agent_harness-1.5.5.5 → 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.5
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`.
@@ -290,6 +301,7 @@ python_agent_harness/
290
301
  ├── cli.py # CLI entry points
291
302
  ├── tui/ # Rich + prompt_toolkit TUI (package)
292
303
  ├── diffrender.py # Unified diff generation + Rich rendering
304
+ ├── lsp/ # Built-in LSP client
293
305
  ├── mcp/ # Optional MCP client
294
306
  └── tools/ # Tool implementations + registry
295
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`.
@@ -256,6 +267,7 @@ python_agent_harness/
256
267
  ├── cli.py # CLI entry points
257
268
  ├── tui/ # Rich + prompt_toolkit TUI (package)
258
269
  ├── diffrender.py # Unified diff generation + Rich rendering
270
+ ├── lsp/ # Built-in LSP client
259
271
  ├── mcp/ # Optional MCP client
260
272
  └── tools/ # Tool implementations + registry
261
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.5"
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.5"
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
+ ]
@@ -0,0 +1,314 @@
1
+ """Small, dependency-free LSP/JSON-RPC client used by the agent harness.
2
+
3
+ This intentionally implements only the transport/lifecycle pieces needed by the
4
+ agent-facing LSP tool. It is synchronous at the public boundary because the
5
+ harness' Tool API is synchronous; a reader thread keeps server messages flowing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import contextlib
11
+ import json
12
+ import os
13
+ import queue
14
+ import subprocess
15
+ import threading
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+
20
+ class LSPError(RuntimeError):
21
+ pass
22
+
23
+
24
+ class LSPClient:
25
+ def __init__(self, command: list[str], root: str, language_id: str, timeout: float = 30.0):
26
+ self.command = command
27
+ self.root = os.path.realpath(root)
28
+ self.language_id = language_id
29
+ self.timeout = timeout
30
+ self.proc: subprocess.Popen[bytes] | None = None
31
+ self._reader: threading.Thread | None = None
32
+ self._write_lock = threading.Lock()
33
+ self._state_lock = threading.Lock()
34
+ self._next_id = 1
35
+ self._pending: dict[int, queue.Queue] = {}
36
+ self._opened: dict[str, int] = {}
37
+ self._texts: dict[str, str] = {}
38
+ self._diagnostics: dict[str, list[dict[str, Any]]] = {}
39
+ self._capabilities: dict[str, Any] = {}
40
+ self.position_encoding = "utf-16"
41
+ self._closed = False
42
+
43
+ @property
44
+ def alive(self) -> bool:
45
+ return self.proc is not None and self.proc.poll() is None and not self._closed
46
+
47
+ def start(self) -> None:
48
+ if self.alive:
49
+ return
50
+ self._closed = False
51
+ try:
52
+ self.proc = subprocess.Popen(
53
+ self.command,
54
+ stdin=subprocess.PIPE,
55
+ stdout=subprocess.PIPE,
56
+ stderr=subprocess.DEVNULL,
57
+ cwd=self.root,
58
+ bufsize=0,
59
+ )
60
+ except OSError as e:
61
+ raise LSPError(f"failed to start LSP server {' '.join(self.command)!r}: {e}") from e
62
+ self._reader = threading.Thread(
63
+ target=self._read_loop, daemon=True, name=f"lsp-reader:{self.command[0]}"
64
+ )
65
+ self._reader.start()
66
+ try:
67
+ result = self.request(
68
+ "initialize",
69
+ {
70
+ "processId": os.getpid(),
71
+ "rootUri": Path(self.root).as_uri(),
72
+ "workspaceFolders": [
73
+ {"uri": Path(self.root).as_uri(), "name": Path(self.root).name}
74
+ ],
75
+ "capabilities": {
76
+ "general": {"positionEncodings": ["utf-16", "utf-8"]},
77
+ "workspace": {
78
+ "workspaceFolders": True,
79
+ "symbol": {"dynamicRegistration": False},
80
+ },
81
+ "textDocument": {
82
+ "definition": {"linkSupport": True},
83
+ "references": {},
84
+ "hover": {"contentFormat": ["markdown", "plaintext"]},
85
+ "documentSymbol": {"hierarchicalDocumentSymbolSupport": True},
86
+ "implementation": {"linkSupport": True},
87
+ "callHierarchy": {"dynamicRegistration": False},
88
+ },
89
+ },
90
+ "trace": "off",
91
+ },
92
+ )
93
+ self._capabilities = result.get("capabilities", {}) if isinstance(result, dict) else {}
94
+ encoding = self._capabilities.get("positionEncoding")
95
+ if encoding in {"utf-8", "utf-16", "utf-32"}:
96
+ self.position_encoding = encoding
97
+ self.notify("initialized", {})
98
+ except Exception:
99
+ self.close()
100
+ raise
101
+
102
+ def request(self, method: str, params: Any = None) -> Any:
103
+ self.start_if_needed()
104
+ with self._state_lock:
105
+ request_id = self._next_id
106
+ self._next_id += 1
107
+ waiter: queue.Queue = queue.Queue(maxsize=1)
108
+ self._pending[request_id] = waiter
109
+ message: dict[str, Any] = {"jsonrpc": "2.0", "id": request_id, "method": method}
110
+ if params is not None:
111
+ message["params"] = params
112
+ try:
113
+ self._send(message)
114
+ kind, value = waiter.get(timeout=self.timeout)
115
+ except queue.Empty as e:
116
+ with self._state_lock:
117
+ self._pending.pop(request_id, None)
118
+ raise LSPError(f"LSP request timed out: {method}") from e
119
+ except BaseException:
120
+ # _send() may raise (server down / write failure); drop the
121
+ # now-orphaned pending entry so it does not leak forever.
122
+ with self._state_lock:
123
+ self._pending.pop(request_id, None)
124
+ raise
125
+ if kind == "error":
126
+ code = value.get("code") if isinstance(value, dict) else None
127
+ msg = (
128
+ value.get("message", "unknown LSP error") if isinstance(value, dict) else str(value)
129
+ )
130
+ raise LSPError(f"LSP {method} failed ({code}): {msg}")
131
+ return value
132
+
133
+ def notify(self, method: str, params: Any = None) -> None:
134
+ self.start_if_needed()
135
+ message: dict[str, Any] = {"jsonrpc": "2.0", "method": method}
136
+ if params is not None:
137
+ message["params"] = params
138
+ self._send(message)
139
+
140
+ def start_if_needed(self) -> None:
141
+ if not self.alive:
142
+ self.start()
143
+
144
+ def open_document(self, uri: str, text: str) -> None:
145
+ self.start_if_needed()
146
+ # Decide which notification to send while holding _state_lock, but
147
+ # send it AFTER releasing the lock: notify()->_send() does a blocking
148
+ # write on the server's stdin pipe, and holding _state_lock across that
149
+ # write can deadlock the reader thread (which needs _state_lock to
150
+ # deliver responses/diagnostics) if the pipe buffer fills.
151
+ with self._state_lock:
152
+ version = self._opened.get(uri, 0)
153
+ if version:
154
+ if self._texts.get(uri) == text:
155
+ return
156
+ version += 1
157
+ self._opened[uri] = version
158
+ self._texts[uri] = text
159
+ method = "textDocument/didChange"
160
+ params: dict[str, Any] = {
161
+ "textDocument": {"uri": uri, "version": version},
162
+ "contentChanges": [{"text": text}],
163
+ }
164
+ else:
165
+ self._opened[uri] = 1
166
+ self._texts[uri] = text
167
+ method = "textDocument/didOpen"
168
+ params = {
169
+ "textDocument": {
170
+ "uri": uri,
171
+ "languageId": self.language_id,
172
+ "version": 1,
173
+ "text": text,
174
+ }
175
+ }
176
+ self.notify(method, params)
177
+
178
+ def close_document(self, uri: str) -> None:
179
+ with self._state_lock:
180
+ if uri not in self._opened:
181
+ return
182
+ self._opened.pop(uri, None)
183
+ self._texts.pop(uri, None)
184
+ if self.alive:
185
+ self.notify("textDocument/didClose", {"textDocument": {"uri": uri}})
186
+
187
+ def diagnostics(self, uri: str) -> list[dict[str, Any]]:
188
+ with self._state_lock:
189
+ return list(self._diagnostics.get(uri, []))
190
+
191
+ def close(self) -> None:
192
+ if self._closed:
193
+ return
194
+ proc = self.proc
195
+ was_alive = proc is not None and proc.poll() is None
196
+ if was_alive:
197
+ with contextlib.suppress(Exception):
198
+ self.request("shutdown", None)
199
+ with contextlib.suppress(Exception):
200
+ self.notify("exit", None)
201
+ self._closed = True
202
+ proc = self.proc
203
+ if proc is not None and proc.poll() is None:
204
+ with contextlib.suppress(Exception):
205
+ proc.terminate()
206
+ proc.wait(timeout=2)
207
+ with contextlib.suppress(Exception):
208
+ proc.kill()
209
+ # Close the pipe file objects so their file descriptors are
210
+ # released (subprocess does not close them on terminate/kill —
211
+ # leaving them open leaks fds and emits ResourceWarning).
212
+ if proc is not None:
213
+ for stream in (proc.stdin, proc.stdout):
214
+ if stream is not None:
215
+ with contextlib.suppress(Exception):
216
+ stream.close()
217
+ with self._state_lock:
218
+ with contextlib.suppress(queue.Full):
219
+ for waiter in self._pending.values():
220
+ waiter.put_nowait(("error", {"message": "LSP client closed"}))
221
+ self._pending.clear()
222
+ self._opened.clear()
223
+ self._texts.clear()
224
+
225
+ def _send(self, message: dict[str, Any]) -> None:
226
+ data = json.dumps(message, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
227
+ packet = b"Content-Length: " + str(len(data)).encode("ascii") + b"\r\n\r\n" + data
228
+ proc = self.proc
229
+ if proc is None or proc.stdin is None or proc.poll() is not None:
230
+ raise LSPError("LSP server is not running")
231
+ with self._write_lock:
232
+ try:
233
+ proc.stdin.write(packet)
234
+ proc.stdin.flush()
235
+ except OSError as e:
236
+ raise LSPError(f"failed writing to LSP server: {e}") from e
237
+
238
+ def _read_loop(self) -> None:
239
+ try:
240
+ proc = self.proc
241
+ if proc is None or proc.stdout is None:
242
+ return
243
+ stream = proc.stdout
244
+ while not self._closed:
245
+ headers: dict[str, str] = {}
246
+ while True:
247
+ line = stream.readline()
248
+ if not line:
249
+ return
250
+ line = line.rstrip(b"\r\n")
251
+ if not line:
252
+ break
253
+ if b":" in line:
254
+ key, value = line.split(b":", 1)
255
+ headers[key.decode("ascii", "replace").lower()] = value.strip().decode(
256
+ "ascii", "replace"
257
+ )
258
+ try:
259
+ length = int(headers.get("content-length", "0"))
260
+ except ValueError:
261
+ continue
262
+ payload = stream.read(length)
263
+ if len(payload) != length:
264
+ return
265
+ try:
266
+ message = json.loads(payload.decode("utf-8"))
267
+ except (UnicodeDecodeError, json.JSONDecodeError):
268
+ continue
269
+ self._handle_message(message)
270
+ finally:
271
+ with self._state_lock:
272
+ pending = list(self._pending.values())
273
+ self._pending.clear()
274
+ with contextlib.suppress(queue.Full):
275
+ for waiter in pending:
276
+ waiter.put_nowait(("error", {"message": "LSP server exited"}))
277
+
278
+ def _handle_message(self, message: dict[str, Any]) -> None:
279
+ if "id" in message and ("result" in message or "error" in message):
280
+ request_id = message.get("id")
281
+ if isinstance(request_id, int):
282
+ with self._state_lock:
283
+ waiter = self._pending.pop(request_id, None)
284
+ if waiter is not None:
285
+ waiter.put(
286
+ ("error", message["error"])
287
+ if "error" in message
288
+ else ("result", message.get("result"))
289
+ )
290
+ return
291
+
292
+ method = message.get("method")
293
+ params = message.get("params")
294
+ if method == "textDocument/publishDiagnostics" and isinstance(params, dict):
295
+ uri = params.get("uri")
296
+ if isinstance(uri, str):
297
+ with self._state_lock:
298
+ self._diagnostics[uri] = params.get("diagnostics", []) or []
299
+ return
300
+
301
+ # Servers may send requests/notifications to the client. Respond to
302
+ # common client-side requests so a server does not stall waiting for
303
+ # configuration/progress/UI handling that the coding agent does not need.
304
+ if "id" in message and isinstance(message.get("id"), int):
305
+ method = str(method or "")
306
+ if method == "workspace/configuration":
307
+ items = params if isinstance(params, list) else []
308
+ result = [None for _ in items]
309
+ elif method == "workspace/applyEdit":
310
+ result = {"applied": False}
311
+ else:
312
+ result = None
313
+ with contextlib.suppress(LSPError):
314
+ self._send({"jsonrpc": "2.0", "id": message["id"], "result": result})