python-agent-harness 1.5.4.3__tar.gz → 1.5.4.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 (92) hide show
  1. {python_agent_harness-1.5.4.3/python_agent_harness.egg-info → python_agent_harness-1.5.4.6}/PKG-INFO +11 -13
  2. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/README.md +10 -12
  3. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/pyproject.toml +1 -1
  4. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/__init__.py +1 -1
  5. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/agent.py +1 -1
  6. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/cli.py +6 -13
  7. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/client.py +18 -9
  8. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/config.py +89 -63
  9. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/persistence.py +0 -3
  10. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/session.py +7 -8
  11. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/token_estimator.py +3 -3
  12. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/__init__.py +13 -6
  13. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/agent_tool.py +1 -1
  14. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/filesystem.py +4 -0
  15. python_agent_harness-1.5.4.6/python_agent_harness/tools/glob_mac.py +97 -0
  16. python_agent_harness-1.5.4.6/python_agent_harness/tools/grep_mac.py +92 -0
  17. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6/python_agent_harness.egg-info}/PKG-INFO +11 -13
  18. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness.egg-info/SOURCES.txt +2 -0
  19. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_cli.py +1 -3
  20. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_client.py +43 -52
  21. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_config.py +141 -83
  22. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_filesystem.py +257 -0
  23. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_persistence.py +12 -15
  24. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_scenarios.py +0 -1
  25. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_session.py +6 -8
  26. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_subagent.py +0 -1
  27. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_subagent_isolation.py +1 -1
  28. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_token_estimator.py +6 -6
  29. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/LICENSE +0 -0
  30. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/__main__.py +0 -0
  31. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/commands.py +0 -0
  32. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/context_manager.py +0 -0
  33. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/diffrender.py +0 -0
  34. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/mcp/__init__.py +0 -0
  35. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/mcp/client.py +0 -0
  36. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/mcp/config.py +0 -0
  37. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/mcp/manager.py +0 -0
  38. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/models.py +0 -0
  39. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/planmode.py +0 -0
  40. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/agent.md +0 -0
  41. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/build-switch.md +0 -0
  42. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/commands/explain.md +0 -0
  43. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/compact.md +0 -0
  44. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/initialize.md +0 -0
  45. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/plan-mode.md +0 -0
  46. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/plan.md +0 -0
  47. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/review.md +0 -0
  48. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/subagent.md +0 -0
  49. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/summary.md +0 -0
  50. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/task-completion-rules.md +0 -0
  51. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/title.md +0 -0
  52. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts.py +0 -0
  53. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/subagent.py +0 -0
  54. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tool_runner.py +0 -0
  55. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/base.py +0 -0
  56. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/bash.py +0 -0
  57. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/diffapply.py +0 -0
  58. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/edit.py +0 -0
  59. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/edit_mac.py +0 -0
  60. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/glob.py +0 -0
  61. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/grep.py +0 -0
  62. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/insert.py +0 -0
  63. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/mcp.py +0 -0
  64. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/mkdir.py +0 -0
  65. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/planexit.py +0 -0
  66. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/question.py +0 -0
  67. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/read.py +0 -0
  68. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/skill.py +0 -0
  69. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/todo.py +0 -0
  70. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/write.py +0 -0
  71. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tui/__init__.py +0 -0
  72. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tui/commands.py +0 -0
  73. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tui/core.py +0 -0
  74. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tui/input.py +0 -0
  75. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness/tui/render.py +0 -0
  76. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness.egg-info/dependency_links.txt +0 -0
  77. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness.egg-info/entry_points.txt +0 -0
  78. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness.egg-info/requires.txt +0 -0
  79. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/python_agent_harness.egg-info/top_level.txt +0 -0
  80. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/setup.cfg +0 -0
  81. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_agents_md.py +0 -0
  82. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_commands.py +0 -0
  83. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_concurrent_subagents.py +0 -0
  84. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_context_rules.py +0 -0
  85. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_diffrender.py +0 -0
  86. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_invariants.py +0 -0
  87. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_mcp.py +0 -0
  88. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_models.py +0 -0
  89. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_planmode.py +0 -0
  90. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_prompts.py +0 -0
  91. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_todos_scope.py +0 -0
  92. {python_agent_harness-1.5.4.3 → python_agent_harness-1.5.4.6}/tests/test_tools_misc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-agent-harness
3
- Version: 1.5.4.3
3
+ Version: 1.5.4.6
4
4
  Summary: A lightweight, hackable mini-OpenCode written in Python.
5
5
  Author: huming
6
6
  License-Expression: MIT
@@ -138,17 +138,15 @@ All LLM settings live in a single JSON configuration file. Environment variables
138
138
  "_comment": "Named LLM profiles for /model switching. Partial settings; unset keys inherit the main llm.",
139
139
  "deepseek": {
140
140
  "base_url": "https://api.deepseek.com/v1",
141
- "model": "deepseek-chat"
141
+ "model": "deepseek-chat",
142
+ "context_window": 128000
142
143
  },
143
144
  "qwen": {
144
145
  "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
145
- "model": "qwen3.5-coder"
146
+ "model": "qwen3.5-coder",
147
+ "context_window": 131072
146
148
  }
147
149
  },
148
- "context_windows": {
149
- "_comment": "Optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g. deepseek-v4 = 1000000); matched in file order, first match wins. Overrides the built-in CONTEXT_WINDOWS table in config.py.",
150
- "deepseek-v4": 1000000
151
- },
152
150
  "subagent_llm": {
153
151
  "profile": null,
154
152
  "base_url": null,
@@ -158,7 +156,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
158
156
  "max_tokens": null,
159
157
  "timeout": null,
160
158
  "reasoning_effort": null,
161
- "stream": null
159
+ "stream": null,
160
+ "context_window": null
162
161
  },
163
162
  "paths": {
164
163
  "context_path": null,
@@ -186,13 +185,12 @@ All LLM settings live in a single JSON configuration file. Environment variables
186
185
 
187
186
  ### Configuration options
188
187
 
189
- - **`llm`** — main LLM configuration. Optional keys include `backend`, `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`.
190
- - **`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.
191
- - **`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`.
192
- - **`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.
188
+ - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, `context_window`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `context_window` (tokens) overrides the built-in table for the active model; when unset, well-known models are matched automatically. `run --no-stream` overrides `stream`.
189
+ - **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`. Each profile can include `context_window` to set the context window for that model. `default` restores the main LLM configuration.
190
+ - **`subagent_llm`** — LLM configuration for `Agent` tool requests. Unset values inherit from the main `llm`. Set `profile` to reuse a profile from `models`. `context_window` can also be set here to override the inherited value for sub-agents. Precedence is: profile settings > explicit `subagent_llm` settings > main `llm` > environment variables.
193
191
  - **`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.
194
192
  - **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
195
- - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`, `_BACKEND`).
193
+ - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
196
194
  - **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
197
195
  - **LLM logging** — request and response bodies are logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change the directory. The log path is printed at startup.
198
196
 
@@ -115,17 +115,15 @@ All LLM settings live in a single JSON configuration file. Environment variables
115
115
  "_comment": "Named LLM profiles for /model switching. Partial settings; unset keys inherit the main llm.",
116
116
  "deepseek": {
117
117
  "base_url": "https://api.deepseek.com/v1",
118
- "model": "deepseek-chat"
118
+ "model": "deepseek-chat",
119
+ "context_window": 128000
119
120
  },
120
121
  "qwen": {
121
122
  "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
122
- "model": "qwen3.5-coder"
123
+ "model": "qwen3.5-coder",
124
+ "context_window": 131072
123
125
  }
124
126
  },
125
- "context_windows": {
126
- "_comment": "Optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g. deepseek-v4 = 1000000); matched in file order, first match wins. Overrides the built-in CONTEXT_WINDOWS table in config.py.",
127
- "deepseek-v4": 1000000
128
- },
129
127
  "subagent_llm": {
130
128
  "profile": null,
131
129
  "base_url": null,
@@ -135,7 +133,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
135
133
  "max_tokens": null,
136
134
  "timeout": null,
137
135
  "reasoning_effort": null,
138
- "stream": null
136
+ "stream": null,
137
+ "context_window": null
139
138
  },
140
139
  "paths": {
141
140
  "context_path": null,
@@ -163,13 +162,12 @@ All LLM settings live in a single JSON configuration file. Environment variables
163
162
 
164
163
  ### Configuration options
165
164
 
166
- - **`llm`** — main LLM configuration. Optional keys include `backend`, `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`.
167
- - **`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.
168
- - **`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`.
169
- - **`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.
165
+ - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, `context_window`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `context_window` (tokens) overrides the built-in table for the active model; when unset, well-known models are matched automatically. `run --no-stream` overrides `stream`.
166
+ - **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`. Each profile can include `context_window` to set the context window for that model. `default` restores the main LLM configuration.
167
+ - **`subagent_llm`** — LLM configuration for `Agent` tool requests. Unset values inherit from the main `llm`. Set `profile` to reuse a profile from `models`. `context_window` can also be set here to override the inherited value for sub-agents. Precedence is: profile settings > explicit `subagent_llm` settings > main `llm` > environment variables.
170
168
  - **`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.
171
169
  - **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
172
- - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`, `_BACKEND`).
170
+ - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
173
171
  - **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
174
172
  - **LLM logging** — request and response bodies are logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change the directory. The log path is printed at startup.
175
173
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-agent-harness"
7
- version = "1.5.4.3"
7
+ version = "1.5.4.6"
8
8
  description = "A lightweight, hackable mini-OpenCode written in Python."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -5,7 +5,7 @@ from .mcp.manager import MCPManager
5
5
  from .models import AgentMode, Message, ToolCall, ToolSpec
6
6
  from .session import Session
7
7
 
8
- __version__ = "1.5.4.3"
8
+ __version__ = "1.5.4.6"
9
9
 
10
10
  __all__ = [
11
11
  "Session",
@@ -452,7 +452,7 @@ class AgentLoop:
452
452
  )
453
453
  # sub-agent runs use their own LLM when one is configured
454
454
  # (a per-invocation clone of session.subagent_client,
455
- # mirroring gptel-agent-harness-subagent-model/-backend);
455
+ # mirroring gptel-agent-harness-subagent-model);
456
456
  # everything unset inherits the main agent's settings, so
457
457
  # the sub-agent path is identical when no separate LLM is
458
458
  # configured
@@ -54,8 +54,8 @@ def make_session(
54
54
  config_path=config_path,
55
55
  )
56
56
  # A separate client for sub-agent requests only when a different
57
- # LLM is configured (mirrors gptel-agent-harness-subagent-model /
58
- # -backend); otherwise the sub-agent shares the main client.
57
+ # LLM is configured (mirrors gptel-agent-harness-subagent-model);
58
+ # otherwise the sub-agent shares the main client.
59
59
  subagent_client = None
60
60
  if any(
61
61
  subagent_settings[k] != settings[k] for k in ("base_url", "api_key", "model", "timeout")
@@ -101,7 +101,6 @@ def make_session(
101
101
  project_dir=abs_project,
102
102
  client=client,
103
103
  model=model,
104
- backend=settings["backend"],
105
104
  system_prompt=system_prompt,
106
105
  subagent_system_prompt=subagent_system_prompt,
107
106
  temperature=settings["temperature"],
@@ -176,7 +175,7 @@ def cmd_config(args: argparse.Namespace) -> int:
176
175
  print(f"config file: {path}")
177
176
  if not path.exists():
178
177
  print("(file does not exist yet — run `python-agent-harness config --init` to create it)")
179
- for key in ("base_url", "model", "backend"):
178
+ for key in ("base_url", "model"):
180
179
  print(f"{key}: {settings[key]}")
181
180
  print(f"api_key: {config.mask_secret(settings['api_key'])}")
182
181
  print(f"temperature: {settings['temperature']}")
@@ -218,17 +217,11 @@ def cmd_config(args: argparse.Namespace) -> int:
218
217
  for name, profile in sorted(model_profiles.items()):
219
218
  model_name = profile.get("model", "(inherited)")
220
219
  base_url = profile.get("base_url", "(inherited)")
221
- print(f" {name}: model={model_name}, base_url={base_url}")
220
+ cw = profile.get("context_window")
221
+ cw_str = f", context_window={cw}" if cw is not None else ""
222
+ print(f" {name}: model={model_name}, base_url={base_url}{cw_str}")
222
223
  else:
223
224
  print("models: (none configured — add a 'models' section to use /model)")
224
- # Show context-window overrides
225
- context_windows = config.load_context_windows_config(args.path)
226
- if context_windows:
227
- print("context_windows:")
228
- for pattern, size in context_windows:
229
- print(f" {pattern}: {size}")
230
- else:
231
- print("context_windows: (none configured — built-in table in config.py applies)")
232
225
  return 0
233
226
 
234
227
 
@@ -217,6 +217,10 @@ class Client:
217
217
  # the user asked to stop. Cleared at the start of each chat()
218
218
  # so a fresh turn may retry normally.
219
219
  self._aborted = False
220
+ # Per-model context-window override: set by the session when
221
+ # switching to a model profile that carries its own
222
+ # context_window. None means "resolve from config / table".
223
+ self._context_window: int | None = None
220
224
  # an explicit log file is inherited by clones so every request
221
225
  # of one session (main + all sub-agents) lands in a single log
222
226
  self.log_path = (
@@ -229,18 +233,21 @@ class Client:
229
233
  def context_window(self) -> int:
230
234
  """Get the context window for this model.
231
235
 
232
- Resolution order: config-file ``context_windows`` overrides
233
- (via ``config.get_context_window_for_model``) -> CONTEXT_WINDOWS
234
- pattern match -> DEFAULT_CONTEXT_WINDOW. Resolved on every
235
- access (no caching), so a runtime model switch or config-file
236
- edit takes effect immediately; a malformed config falls back to
237
- the default for that access and recovers once the file is fixed.
236
+ Resolution order: explicit ``_context_window`` override (set by
237
+ the session on model switch) → ``llm.context_window`` from the
238
+ config file built-in CONTEXT_WINDOWS table (substring match)
239
+ DEFAULT_CONTEXT_WINDOW. Resolved on every access (no
240
+ caching), so a runtime model switch or config-file edit takes
241
+ effect immediately; a malformed config falls back to the default
242
+ for that access and recovers once the file is fixed.
238
243
  """
244
+ if self._context_window is not None:
245
+ return self._context_window
239
246
  try:
240
247
  return config.get_context_window_for_model(self.model, config_path=self._config_path)
241
248
  except Exception:
242
- # a malformed context_windows section must not break the
243
- # loop: use the safe default, retry on the next access
249
+ # a malformed config must not break the loop: use the safe
250
+ # default, retry on the next access
244
251
  return config.DEFAULT_CONTEXT_WINDOW
245
252
 
246
253
  def close(self) -> None:
@@ -259,7 +266,7 @@ class Client:
259
266
  flag strictly per-request. The log file is shared so one
260
267
  session's LLM interactions stay in one log.
261
268
  """
262
- return Client(
269
+ c = Client(
263
270
  base_url=self.base_url,
264
271
  api_key=self.api_key,
265
272
  model=self.model,
@@ -271,6 +278,8 @@ class Client:
271
278
  config_path=self._config_path,
272
279
  log_path=self.log_path,
273
280
  )
281
+ c._context_window = self._context_window
282
+ return c
274
283
 
275
284
  def abort(self) -> None:
276
285
  """Abort the in-flight request (called on cancel).
@@ -7,6 +7,7 @@ from __future__ import annotations
7
7
 
8
8
  import json
9
9
  import os
10
+ import warnings
10
11
  from pathlib import Path
11
12
 
12
13
  from .mcp.config import MCPConfig
@@ -168,12 +169,12 @@ DEFAULT_LLM: dict = {
168
169
  "base_url": "https://api.openai.com/v1",
169
170
  "api_key": None,
170
171
  "model": "gpt-5-mini",
171
- "backend": "OpenAI-compatible",
172
172
  "temperature": TEMPERATURE,
173
173
  "max_tokens": MAX_TOKENS,
174
174
  "timeout": 600.0,
175
175
  "reasoning_effort": None,
176
176
  "stream": True,
177
+ "context_window": None,
177
178
  }
178
179
 
179
180
  DEFAULT_PATHS: dict = {
@@ -182,8 +183,7 @@ DEFAULT_PATHS: dict = {
182
183
  }
183
184
 
184
185
  # Sub-agent LLM overrides: every key defaults to None, meaning "inherit
185
- # the main LLM setting" (mirrors gptel-agent-harness-subagent-model /
186
- # -backend). Only the keys the user actually sets differ from the main
186
+ # the main LLM setting" (mirrors gptel-agent-harness-subagent-model).
187
187
  # agent's LLM. ``profile`` references a named profile from the
188
188
  # ``models`` section: its settings are applied on top of any explicit
189
189
  # subagent_llm keys (profile wins), and unset keys still inherit the
@@ -193,12 +193,12 @@ DEFAULT_SUBAGENT_LLM: dict = {
193
193
  "base_url": None,
194
194
  "api_key": None,
195
195
  "model": None,
196
- "backend": None,
197
196
  "temperature": None,
198
197
  "max_tokens": None,
199
198
  "timeout": None,
200
199
  "reasoning_effort": None,
201
200
  "stream": None,
201
+ "context_window": None,
202
202
  }
203
203
 
204
204
  CONFIG_TEMPLATE = """\
@@ -211,20 +211,18 @@ CONFIG_TEMPLATE = """\
211
211
  "stream": true
212
212
  }},
213
213
  "models": {{
214
- "_comment": "Named LLM profiles for /model switching. Each entry is a full set of LLM settings (base_url, api_key, model, etc.). Use /model in the TUI to switch at runtime.",
214
+ "_comment": "Named LLM profiles for /model switching. Each entry is a full set of LLM settings (base_url, api_key, model, context_window, etc.). Use /model in the TUI to switch at runtime. Unset keys inherit the main llm settings.",
215
215
  "deepseek": {{
216
216
  "base_url": "https://api.deepseek.com/v1",
217
- "model": "deepseek-chat"
217
+ "model": "deepseek-chat",
218
+ "context_window": 128000
218
219
  }},
219
220
  "openai": {{
220
221
  "base_url": "https://api.openai.com/v1",
221
- "model": "gpt-5-mini"
222
+ "model": "gpt-5-mini",
223
+ "context_window": 128000
222
224
  }}
223
225
  }},
224
- "context_windows": {{
225
- "_comment": "Optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g. deepseek-v4 = 1000000); matched in file order, first match wins. Overrides the built-in CONTEXT_WINDOWS table in config.py. Remove this section to use the built-in table.",
226
- "deepseek-v4": 1000000
227
- }},
228
226
  "subagent_llm": {{
229
227
  "_comment": "Optional overrides for sub-agent (Agent tool) requests, e.g. a cheaper model. Every key is optional; unset keys inherit the main llm settings above. Set 'profile' to a name from the 'models' section to reuse a model profile (profile settings win over explicit keys below).",
230
228
  "profile": null,
@@ -235,7 +233,8 @@ CONFIG_TEMPLATE = """\
235
233
  "max_tokens": null,
236
234
  "timeout": null,
237
235
  "reasoning_effort": null,
238
- "stream": null
236
+ "stream": null,
237
+ "context_window": null
239
238
  }},
240
239
  "paths": {{
241
240
  "_comment": "Optional overrides for context and skill directories. Absolute paths or ~ expansion supported.",
@@ -264,17 +263,31 @@ _ENV_OVERRIDES = {
264
263
  "base_url": "OPENAI_BASE_URL",
265
264
  "api_key": "OPENAI_API_KEY",
266
265
  "model": "OPENAI_MODEL",
267
- "backend": "OPENAI_BACKEND",
268
266
  }
269
267
 
270
268
  _SUBAGENT_ENV_OVERRIDES = {
271
269
  "base_url": "OPENAI_SUBAGENT_BASE_URL",
272
270
  "api_key": "OPENAI_SUBAGENT_API_KEY",
273
271
  "model": "OPENAI_SUBAGENT_MODEL",
274
- "backend": "OPENAI_SUBAGENT_BACKEND",
275
272
  }
276
273
 
277
274
 
275
+ def _validate_context_window(
276
+ value: object, section: str, path: str | os.PathLike | None = None
277
+ ) -> None:
278
+ """Raise ValueError if *value* is set but not a positive integer."""
279
+ if value is None:
280
+ return
281
+ if isinstance(value, bool) or not isinstance(value, int):
282
+ raise ValueError(
283
+ f"config file {_config_path(path)}: {section}.context_window must be an integer"
284
+ )
285
+ if value <= 0:
286
+ raise ValueError(
287
+ f"config file {_config_path(path)}: {section}.context_window must be positive"
288
+ )
289
+
290
+
278
291
  def _config_path(path: str | os.PathLike | None = None) -> Path:
279
292
  """Resolve the config file path: explicit arg > $PYTHON_AGENT_HARNESS_CONFIG > default."""
280
293
  if path:
@@ -310,6 +323,9 @@ def load_llm_config(path: str | os.PathLike | None = None) -> dict:
310
323
 
311
324
  The config file is JSON with an ``llm`` object (see `CONFIG_TEMPLATE`).
312
325
  Environment variables still win if set, so existing setups keep working.
326
+ ``context_window`` is an optional integer (tokens); when unset it
327
+ defaults to None, meaning "resolve from the built-in table at
328
+ runtime".
313
329
  """
314
330
  settings = dict(DEFAULT_LLM)
315
331
  data = _read_config(path)
@@ -320,15 +336,24 @@ def load_llm_config(path: str | os.PathLike | None = None) -> dict:
320
336
  "base_url",
321
337
  "api_key",
322
338
  "model",
323
- "backend",
324
339
  "temperature",
325
340
  "max_tokens",
326
341
  "timeout",
327
342
  "reasoning_effort",
328
343
  "stream",
344
+ "context_window",
329
345
  ):
330
346
  if key in llm and llm[key] is not None:
331
347
  settings[key] = llm[key]
348
+ _validate_context_window(settings.get("context_window"), "llm", path)
349
+ # Warn if the old top-level context_windows section is still present.
350
+ if data.get("context_windows"):
351
+ warnings.warn(
352
+ "The top-level 'context_windows' section in config.json is deprecated and ignored. "
353
+ "Set 'context_window' inside 'llm' or each model profile in 'models' instead.",
354
+ DeprecationWarning,
355
+ stacklevel=2,
356
+ )
332
357
  for key, env in _ENV_OVERRIDES.items():
333
358
  val = os.environ.get(env)
334
359
  if val:
@@ -342,7 +367,7 @@ def load_subagent_llm_config(
342
367
  ) -> dict:
343
368
  """Resolve sub-agent LLM settings; unset keys inherit ``main``.
344
369
 
345
- Mirrors gptel-agent-harness-subagent-model/-backend: sub-agents
370
+ Mirrors gptel-agent-harness-subagent-model: sub-agents
346
371
  (the Agent tool) use their own LLM when configured, otherwise the
347
372
  main agent's. Precedence: ``main`` settings < config file
348
373
  ``subagent_llm`` object < referenced ``models`` profile (when
@@ -379,12 +404,12 @@ def load_subagent_llm_config(
379
404
  "base_url",
380
405
  "api_key",
381
406
  "model",
382
- "backend",
383
407
  "temperature",
384
408
  "max_tokens",
385
409
  "timeout",
386
410
  "reasoning_effort",
387
411
  "stream",
412
+ "context_window",
388
413
  ):
389
414
  if key in profile and profile[key] is not None:
390
415
  overrides[key] = profile[key]
@@ -480,61 +505,62 @@ def _match_context_window(model: str) -> int | None:
480
505
  return None
481
506
 
482
507
 
483
- def load_context_windows_config(
484
- path: str | os.PathLike | None = None,
485
- ) -> list[tuple[str, int]]:
486
- """Load per-model context-window overrides from the config file.
487
-
488
- Reads the ``context_windows`` object: a mapping of model names or
489
- substrings (matched in file order, first match wins) to token
490
- counts. Keys starting with ``_`` are comments and skipped. A
491
- missing file, missing section, or unreadable JSON yields ``[]``
492
- (callers fall back to the built-in table); a malformed section or
493
- non-integer size raises ValueError so config errors surface.
508
+ def get_context_window_for_model(
509
+ model: str,
510
+ config_path: str | os.PathLike | None = None,
511
+ ) -> int:
512
+ """Get the context window for MODEL.
513
+
514
+ Resolution order:
515
+ 1. ``llm.context_window`` from the config file (when the active
516
+ model matches this is already baked into the settings dict
517
+ returned by ``load_llm_config``; callers that use a resolved
518
+ settings dict should prefer ``resolve_context_window`` instead).
519
+ 2. Built-in CONTEXT_WINDOWS table (substring match, first wins).
520
+ 3. DEFAULT_CONTEXT_WINDOW.
521
+
522
+ This function is the fallback for callers that only have a model
523
+ name and no resolved settings dict (e.g. ``token_estimator``
524
+ or ``FakeClient``).
494
525
  """
526
+ # Try the llm.context_window from the config file first.
495
527
  try:
496
- data = _read_config(path)
528
+ data = _read_config(config_path)
497
529
  except ValueError:
498
- return []
499
- section = data.get("context_windows") or {}
500
- if not isinstance(section, dict):
501
- raise ValueError(f"config file {_config_path(path)}: context_windows must be an object")
502
- entries: list[tuple[str, int]] = []
503
- for pattern, size in section.items():
504
- if pattern.startswith("_"):
505
- continue
506
- if isinstance(size, bool) or not isinstance(size, int):
507
- raise ValueError(
508
- f"config file {_config_path(path)}: context_windows.{pattern} must be an integer"
509
- )
510
- entries.append((pattern, size))
511
- return entries
530
+ data = {}
531
+ llm = data.get("llm") or {}
532
+ if isinstance(llm, dict):
533
+ cw = llm.get("context_window")
534
+ if cw is not None and isinstance(cw, int) and not isinstance(cw, bool) and cw > 0:
535
+ return cw
536
+ # Fall back to the built-in table, then the default.
537
+ matched = _match_context_window(model)
538
+ if matched is not None:
539
+ return matched
540
+ return DEFAULT_CONTEXT_WINDOW
512
541
 
513
542
 
514
- def get_context_window_for_model(
515
- model: str,
516
- config_path: str | os.PathLike | None = None,
517
- ) -> int:
518
- """Get the context window for MODEL: config-file overrides, then
519
- the built-in table, then the default.
543
+ def resolve_context_window(settings: dict, model: str | None = None) -> int:
544
+ """Resolve the context window from a fully-merged settings dict.
520
545
 
521
- The config file's ``context_windows`` object (user overrides) is
522
- consulted first (substring match over its keys, first match wins,
523
- case-insensitive); then the CONTEXT_WINDOWS table in config.py;
524
- then DEFAULT_CONTEXT_WINDOW.
546
+ Precedence:
547
+ 1. ``settings["context_window"]`` set by the active profile,
548
+ ``llm`` section, or ``subagent_llm`` section (already merged
549
+ by the load functions).
550
+ 2. Built-in CONTEXT_WINDOWS table (substring match on *model*).
551
+ 3. DEFAULT_CONTEXT_WINDOW.
525
552
 
526
553
  Args:
527
- model: The model ID to look up (e.g. "deepseek-v4-flash")
528
- config_path: Optional path to the config file; defaults to the
529
- standard config location (config.json).
530
-
531
- Returns:
532
- The context window size as an integer.
554
+ settings: A resolved LLM settings dict (from ``load_llm_config``,
555
+ ``load_subagent_llm_config``, or a merged profile).
556
+ model: The model ID to match against the built-in table when
557
+ ``context_window`` is not set. Defaults to
558
+ ``settings["model"]`` when omitted.
533
559
  """
534
- lowered = model.lower()
535
- for pattern, size in load_context_windows_config(config_path):
536
- if pattern.lower() in lowered:
537
- return size
560
+ cw = settings.get("context_window")
561
+ if cw is not None and isinstance(cw, int) and not isinstance(cw, bool) and cw > 0:
562
+ return cw
563
+ model = model or settings.get("model") or ""
538
564
  matched = _match_context_window(model)
539
565
  if matched is not None:
540
566
  return matched
@@ -107,7 +107,6 @@ class SessionPersistence:
107
107
  self,
108
108
  project_dir: str,
109
109
  model: str,
110
- backend: str,
111
110
  system_prompt: str | None = None,
112
111
  temperature: float | None = None,
113
112
  max_tokens: int | None = None,
@@ -116,7 +115,6 @@ class SessionPersistence:
116
115
  ) -> None:
117
116
  self.project_dir = project_dir
118
117
  self.model = model
119
- self.backend = backend
120
118
  self.system_prompt = system_prompt
121
119
  self.temperature = temperature
122
120
  self.max_tokens = max_tokens
@@ -173,7 +171,6 @@ class SessionPersistence:
173
171
  pairs = [
174
172
  ("python-agent-harness--project-dir", self.project_dir),
175
173
  ("gptel-model", self.model),
176
- ("gptel--backend-name", self.backend),
177
174
  ("gptel-system-prompt", self.system_prompt),
178
175
  ("gptel-temperature", self.temperature),
179
176
  ("gptel-max-tokens", self.max_tokens),
@@ -71,7 +71,6 @@ class Session:
71
71
  project_dir: str,
72
72
  client: Client,
73
73
  model: str,
74
- backend: str = "OpenAI-compatible",
75
74
  system_prompt: str | None = None,
76
75
  subagent_system_prompt: str | None = None,
77
76
  temperature: float = config.TEMPERATURE,
@@ -96,7 +95,6 @@ class Session:
96
95
  self.project_dir = project_dir
97
96
  self.client = client
98
97
  self.model = model
99
- self.backend = backend
100
98
  self.system_prompt = system_prompt
101
99
  self.subagent_system_prompt = subagent_system_prompt
102
100
  self.temperature = temperature
@@ -113,7 +111,7 @@ class Session:
113
111
  # Sub-agent LLM: a dedicated client (base_url/api_key/model/
114
112
  # timeout) and per-request options when a different LLM is
115
113
  # configured for sub-agents (mirrors gptel-agent-harness-
116
- # subagent-model/-backend); every unset option inherits the
114
+ # subagent-model); every unset option inherits the
117
115
  # main agent's value. The sub-agent loop never uses this
118
116
  # client directly — each Agent tool invocation clones it
119
117
  # (see run_subagent) so concurrent sub-agents never share a
@@ -161,7 +159,6 @@ class Session:
161
159
  self.store = SessionPersistence(
162
160
  project_dir=project_dir,
163
161
  model=model,
164
- backend=backend,
165
162
  system_prompt=system_prompt,
166
163
  temperature=temperature,
167
164
  max_tokens=max_tokens,
@@ -659,12 +656,12 @@ class Session:
659
656
  "base_url": self.client.base_url,
660
657
  "api_key": self.client.api_key,
661
658
  "model": self.model,
662
- "backend": self.backend,
663
659
  "temperature": self.temperature,
664
660
  "max_tokens": self.max_tokens,
665
661
  "timeout": self.client.timeout,
666
662
  "reasoning_effort": self.reasoning_effort,
667
663
  "stream": self.stream,
664
+ "context_window": getattr(self.client, "_context_window", None),
668
665
  }
669
666
  for key, val in current.items():
670
667
  merged.setdefault(key, val)
@@ -673,12 +670,12 @@ class Session:
673
670
  "base_url",
674
671
  "api_key",
675
672
  "model",
676
- "backend",
677
673
  "temperature",
678
674
  "max_tokens",
679
675
  "timeout",
680
676
  "reasoning_effort",
681
677
  "stream",
678
+ "context_window",
682
679
  ):
683
680
  if key in profile and profile[key] is not None:
684
681
  merged[key] = profile[key]
@@ -690,8 +687,6 @@ class Session:
690
687
  # the calibration factor is tokenizer-specific: a factor tuned
691
688
  # to the previous model must not skew estimates for the new one
692
689
  self.calibrator.reset()
693
- self.backend = merged["backend"]
694
- self.store.backend = merged["backend"]
695
690
  self.temperature = merged["temperature"]
696
691
  self.max_tokens = merged["max_tokens"]
697
692
  if hasattr(self.client, "set_timeout"):
@@ -700,6 +695,10 @@ class Session:
700
695
  self.client.timeout = merged["timeout"]
701
696
  self.reasoning_effort = merged["reasoning_effort"]
702
697
  self.stream = merged["stream"]
698
+ # context_window: set the client override so subsequent
699
+ # client.context_window accesses use the profile's value
700
+ # (None means "resolve from config / built-in table").
701
+ self.client._context_window = merged.get("context_window")
703
702
  return True, f"switched to {name} ({self.model})"
704
703
 
705
704
  # ------------------------------------------------------------------
@@ -44,9 +44,9 @@ def context_window_for(model: str, config_path: str | os.PathLike | None = None)
44
44
  """Return the context window for MODEL, or a safe fallback.
45
45
 
46
46
  Delegates to ``config.get_context_window_for_model``: config-file
47
- ``context_windows`` overrides (config.json) first, then substring
48
- match over CONTEXT_WINDOWS (first match wins, case-insensitive),
49
- then DEFAULT_CONTEXT_WINDOW.
47
+ ``llm.context_window`` first, then substring match over
48
+ CONTEXT_WINDOWS (first match wins, case-insensitive), then
49
+ DEFAULT_CONTEXT_WINDOW.
50
50
 
51
51
  Args:
52
52
  model: The model ID to look up.