python-agent-harness 1.5.4.6__tar.gz → 1.5.4.7__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.6/python_agent_harness.egg-info → python_agent_harness-1.5.4.7}/PKG-INFO +12 -10
  2. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/README.md +11 -9
  3. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/pyproject.toml +1 -1
  4. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/__init__.py +1 -1
  5. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/cli.py +9 -3
  6. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/client.py +9 -18
  7. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/config.py +58 -87
  8. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/session.py +0 -6
  9. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/token_estimator.py +3 -3
  10. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7/python_agent_harness.egg-info}/PKG-INFO +12 -10
  11. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_client.py +52 -43
  12. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_config.py +90 -138
  13. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_session.py +2 -6
  14. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_token_estimator.py +6 -6
  15. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/LICENSE +0 -0
  16. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/__main__.py +0 -0
  17. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/agent.py +0 -0
  18. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/commands.py +0 -0
  19. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/context_manager.py +0 -0
  20. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/diffrender.py +0 -0
  21. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/mcp/__init__.py +0 -0
  22. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/mcp/client.py +0 -0
  23. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/mcp/config.py +0 -0
  24. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/mcp/manager.py +0 -0
  25. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/models.py +0 -0
  26. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/persistence.py +0 -0
  27. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/planmode.py +0 -0
  28. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/agent.md +0 -0
  29. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/build-switch.md +0 -0
  30. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/commands/explain.md +0 -0
  31. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/compact.md +0 -0
  32. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/initialize.md +0 -0
  33. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/plan-mode.md +0 -0
  34. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/plan.md +0 -0
  35. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/review.md +0 -0
  36. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/subagent.md +0 -0
  37. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/summary.md +0 -0
  38. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/task-completion-rules.md +0 -0
  39. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts/title.md +0 -0
  40. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/prompts.py +0 -0
  41. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/subagent.py +0 -0
  42. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tool_runner.py +0 -0
  43. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/__init__.py +0 -0
  44. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/agent_tool.py +0 -0
  45. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/base.py +0 -0
  46. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/bash.py +0 -0
  47. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/diffapply.py +0 -0
  48. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/edit.py +0 -0
  49. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/edit_mac.py +0 -0
  50. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/filesystem.py +0 -0
  51. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/glob.py +0 -0
  52. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/glob_mac.py +0 -0
  53. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/grep.py +0 -0
  54. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/grep_mac.py +0 -0
  55. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/insert.py +0 -0
  56. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/mcp.py +0 -0
  57. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/mkdir.py +0 -0
  58. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/planexit.py +0 -0
  59. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/question.py +0 -0
  60. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/read.py +0 -0
  61. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/skill.py +0 -0
  62. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/todo.py +0 -0
  63. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tools/write.py +0 -0
  64. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tui/__init__.py +0 -0
  65. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tui/commands.py +0 -0
  66. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tui/core.py +0 -0
  67. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tui/input.py +0 -0
  68. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness/tui/render.py +0 -0
  69. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness.egg-info/SOURCES.txt +0 -0
  70. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness.egg-info/dependency_links.txt +0 -0
  71. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness.egg-info/entry_points.txt +0 -0
  72. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness.egg-info/requires.txt +0 -0
  73. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/python_agent_harness.egg-info/top_level.txt +0 -0
  74. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/setup.cfg +0 -0
  75. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_agents_md.py +0 -0
  76. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_cli.py +0 -0
  77. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_commands.py +0 -0
  78. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_concurrent_subagents.py +0 -0
  79. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_context_rules.py +0 -0
  80. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_diffrender.py +0 -0
  81. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_filesystem.py +0 -0
  82. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_invariants.py +0 -0
  83. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_mcp.py +0 -0
  84. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_models.py +0 -0
  85. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_persistence.py +0 -0
  86. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_planmode.py +0 -0
  87. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_prompts.py +0 -0
  88. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_scenarios.py +0 -0
  89. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_subagent.py +0 -0
  90. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_subagent_isolation.py +0 -0
  91. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/tests/test_todos_scope.py +0 -0
  92. {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.7}/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.6
3
+ Version: 1.5.4.7
4
4
  Summary: A lightweight, hackable mini-OpenCode written in Python.
5
5
  Author: huming
6
6
  License-Expression: MIT
@@ -138,15 +138,17 @@ 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",
142
- "context_window": 128000
141
+ "model": "deepseek-chat"
143
142
  },
144
143
  "qwen": {
145
144
  "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
146
- "model": "qwen3.5-coder",
147
- "context_window": 131072
145
+ "model": "qwen3.5-coder"
148
146
  }
149
147
  },
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
+ },
150
152
  "subagent_llm": {
151
153
  "profile": null,
152
154
  "base_url": null,
@@ -156,8 +158,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
156
158
  "max_tokens": null,
157
159
  "timeout": null,
158
160
  "reasoning_effort": null,
159
- "stream": null,
160
- "context_window": null
161
+ "stream": null
161
162
  },
162
163
  "paths": {
163
164
  "context_path": null,
@@ -185,9 +186,10 @@ All LLM settings live in a single JSON configuration file. Environment variables
185
186
 
186
187
  ### Configuration options
187
188
 
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.
189
+ - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
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.
191
193
  - **`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.
192
194
  - **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
193
195
  - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
@@ -115,15 +115,17 @@ 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",
119
- "context_window": 128000
118
+ "model": "deepseek-chat"
120
119
  },
121
120
  "qwen": {
122
121
  "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
123
- "model": "qwen3.5-coder",
124
- "context_window": 131072
122
+ "model": "qwen3.5-coder"
125
123
  }
126
124
  },
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
+ },
127
129
  "subagent_llm": {
128
130
  "profile": null,
129
131
  "base_url": null,
@@ -133,8 +135,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
133
135
  "max_tokens": null,
134
136
  "timeout": null,
135
137
  "reasoning_effort": null,
136
- "stream": null,
137
- "context_window": null
138
+ "stream": null
138
139
  },
139
140
  "paths": {
140
141
  "context_path": null,
@@ -162,9 +163,10 @@ All LLM settings live in a single JSON configuration file. Environment variables
162
163
 
163
164
  ### Configuration options
164
165
 
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.
166
+ - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
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.
168
170
  - **`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.
169
171
  - **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
170
172
  - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
@@ -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.6"
7
+ version = "1.5.4.7"
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.6"
8
+ __version__ = "1.5.4.7"
9
9
 
10
10
  __all__ = [
11
11
  "Session",
@@ -217,11 +217,17 @@ def cmd_config(args: argparse.Namespace) -> int:
217
217
  for name, profile in sorted(model_profiles.items()):
218
218
  model_name = profile.get("model", "(inherited)")
219
219
  base_url = profile.get("base_url", "(inherited)")
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}")
220
+ print(f" {name}: model={model_name}, base_url={base_url}")
223
221
  else:
224
222
  print("models: (none configured — add a 'models' section to use /model)")
223
+ # Show context-window overrides
224
+ context_windows = config.load_context_windows_config(args.path)
225
+ if context_windows:
226
+ print("context_windows:")
227
+ for pattern, size in context_windows:
228
+ print(f" {pattern}: {size}")
229
+ else:
230
+ print("context_windows: (none configured — built-in table in config.py applies)")
225
231
  return 0
226
232
 
227
233
 
@@ -217,10 +217,6 @@ 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
224
220
  # an explicit log file is inherited by clones so every request
225
221
  # of one session (main + all sub-agents) lands in a single log
226
222
  self.log_path = (
@@ -233,21 +229,18 @@ class Client:
233
229
  def context_window(self) -> int:
234
230
  """Get the context window for this model.
235
231
 
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.
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.
243
238
  """
244
- if self._context_window is not None:
245
- return self._context_window
246
239
  try:
247
240
  return config.get_context_window_for_model(self.model, config_path=self._config_path)
248
241
  except Exception:
249
- # a malformed config must not break the loop: use the safe
250
- # default, retry on the next access
242
+ # a malformed context_windows section must not break the
243
+ # loop: use the safe default, retry on the next access
251
244
  return config.DEFAULT_CONTEXT_WINDOW
252
245
 
253
246
  def close(self) -> None:
@@ -266,7 +259,7 @@ class Client:
266
259
  flag strictly per-request. The log file is shared so one
267
260
  session's LLM interactions stay in one log.
268
261
  """
269
- c = Client(
262
+ return Client(
270
263
  base_url=self.base_url,
271
264
  api_key=self.api_key,
272
265
  model=self.model,
@@ -278,8 +271,6 @@ class Client:
278
271
  config_path=self._config_path,
279
272
  log_path=self.log_path,
280
273
  )
281
- c._context_window = self._context_window
282
- return c
283
274
 
284
275
  def abort(self) -> None:
285
276
  """Abort the in-flight request (called on cancel).
@@ -7,7 +7,6 @@ from __future__ import annotations
7
7
 
8
8
  import json
9
9
  import os
10
- import warnings
11
10
  from pathlib import Path
12
11
 
13
12
  from .mcp.config import MCPConfig
@@ -174,7 +173,6 @@ DEFAULT_LLM: dict = {
174
173
  "timeout": 600.0,
175
174
  "reasoning_effort": None,
176
175
  "stream": True,
177
- "context_window": None,
178
176
  }
179
177
 
180
178
  DEFAULT_PATHS: dict = {
@@ -198,7 +196,6 @@ DEFAULT_SUBAGENT_LLM: dict = {
198
196
  "timeout": None,
199
197
  "reasoning_effort": None,
200
198
  "stream": None,
201
- "context_window": None,
202
199
  }
203
200
 
204
201
  CONFIG_TEMPLATE = """\
@@ -211,18 +208,20 @@ CONFIG_TEMPLATE = """\
211
208
  "stream": true
212
209
  }},
213
210
  "models": {{
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.",
211
+ "_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.",
215
212
  "deepseek": {{
216
213
  "base_url": "https://api.deepseek.com/v1",
217
- "model": "deepseek-chat",
218
- "context_window": 128000
214
+ "model": "deepseek-chat"
219
215
  }},
220
216
  "openai": {{
221
217
  "base_url": "https://api.openai.com/v1",
222
- "model": "gpt-5-mini",
223
- "context_window": 128000
218
+ "model": "gpt-5-mini"
224
219
  }}
225
220
  }},
221
+ "context_windows": {{
222
+ "_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.",
223
+ "deepseek-v4": 1000000
224
+ }},
226
225
  "subagent_llm": {{
227
226
  "_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).",
228
227
  "profile": null,
@@ -233,8 +232,7 @@ CONFIG_TEMPLATE = """\
233
232
  "max_tokens": null,
234
233
  "timeout": null,
235
234
  "reasoning_effort": null,
236
- "stream": null,
237
- "context_window": null
235
+ "stream": null
238
236
  }},
239
237
  "paths": {{
240
238
  "_comment": "Optional overrides for context and skill directories. Absolute paths or ~ expansion supported.",
@@ -272,22 +270,6 @@ _SUBAGENT_ENV_OVERRIDES = {
272
270
  }
273
271
 
274
272
 
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
-
291
273
  def _config_path(path: str | os.PathLike | None = None) -> Path:
292
274
  """Resolve the config file path: explicit arg > $PYTHON_AGENT_HARNESS_CONFIG > default."""
293
275
  if path:
@@ -323,9 +305,6 @@ def load_llm_config(path: str | os.PathLike | None = None) -> dict:
323
305
 
324
306
  The config file is JSON with an ``llm`` object (see `CONFIG_TEMPLATE`).
325
307
  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".
329
308
  """
330
309
  settings = dict(DEFAULT_LLM)
331
310
  data = _read_config(path)
@@ -341,19 +320,9 @@ def load_llm_config(path: str | os.PathLike | None = None) -> dict:
341
320
  "timeout",
342
321
  "reasoning_effort",
343
322
  "stream",
344
- "context_window",
345
323
  ):
346
324
  if key in llm and llm[key] is not None:
347
325
  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
- )
357
326
  for key, env in _ENV_OVERRIDES.items():
358
327
  val = os.environ.get(env)
359
328
  if val:
@@ -409,7 +378,6 @@ def load_subagent_llm_config(
409
378
  "timeout",
410
379
  "reasoning_effort",
411
380
  "stream",
412
- "context_window",
413
381
  ):
414
382
  if key in profile and profile[key] is not None:
415
383
  overrides[key] = profile[key]
@@ -505,62 +473,65 @@ def _match_context_window(model: str) -> int | None:
505
473
  return None
506
474
 
507
475
 
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``).
476
+ def load_context_windows_config(
477
+ path: str | os.PathLike | None = None,
478
+ ) -> list[tuple[str, int]]:
479
+ """Load per-model context-window overrides from the config file.
480
+
481
+ Reads the ``context_windows`` object: a mapping of model names or
482
+ substrings (matched in file order, first match wins) to token
483
+ counts. Keys starting with ``_`` are comments and skipped. A
484
+ missing file, missing section, or unreadable JSON yields ``[]``
485
+ (callers fall back to the built-in table); a malformed section or
486
+ non-integer size raises ValueError so config errors surface.
525
487
  """
526
- # Try the llm.context_window from the config file first.
527
488
  try:
528
- data = _read_config(config_path)
489
+ data = _read_config(path)
529
490
  except ValueError:
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
491
+ return []
492
+ section = data.get("context_windows") or {}
493
+ if not isinstance(section, dict):
494
+ raise ValueError(f"config file {_config_path(path)}: context_windows must be an object")
495
+ entries: list[tuple[str, int]] = []
496
+ for pattern, size in section.items():
497
+ if pattern.startswith("_"):
498
+ continue
499
+ if isinstance(size, bool) or not isinstance(size, int):
500
+ raise ValueError(
501
+ f"config file {_config_path(path)}: context_windows.{pattern} must be an integer"
502
+ )
503
+ if size <= 0:
504
+ raise ValueError(
505
+ f"config file {_config_path(path)}: context_windows.{pattern} must be positive"
506
+ )
507
+ entries.append((pattern, size))
508
+ return entries
541
509
 
542
510
 
543
- def resolve_context_window(settings: dict, model: str | None = None) -> int:
544
- """Resolve the context window from a fully-merged settings dict.
511
+ def get_context_window_for_model(
512
+ model: str,
513
+ config_path: str | os.PathLike | None = None,
514
+ ) -> int:
515
+ """Get the context window for MODEL: config-file overrides, then
516
+ the built-in table, then the default.
545
517
 
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.
518
+ The config file's ``context_windows`` object (user overrides) is
519
+ consulted first (substring match over its keys, first match wins,
520
+ case-insensitive); then the CONTEXT_WINDOWS table in config.py;
521
+ then DEFAULT_CONTEXT_WINDOW.
552
522
 
553
523
  Args:
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.
524
+ model: The model ID to look up (e.g. "deepseek-v4-flash")
525
+ config_path: Optional path to the config file; defaults to the
526
+ standard config location (config.json).
527
+
528
+ Returns:
529
+ The context window size as an integer.
559
530
  """
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 ""
531
+ lowered = model.lower()
532
+ for pattern, size in load_context_windows_config(config_path):
533
+ if pattern.lower() in lowered:
534
+ return size
564
535
  matched = _match_context_window(model)
565
536
  if matched is not None:
566
537
  return matched
@@ -661,7 +661,6 @@ class Session:
661
661
  "timeout": self.client.timeout,
662
662
  "reasoning_effort": self.reasoning_effort,
663
663
  "stream": self.stream,
664
- "context_window": getattr(self.client, "_context_window", None),
665
664
  }
666
665
  for key, val in current.items():
667
666
  merged.setdefault(key, val)
@@ -675,7 +674,6 @@ class Session:
675
674
  "timeout",
676
675
  "reasoning_effort",
677
676
  "stream",
678
- "context_window",
679
677
  ):
680
678
  if key in profile and profile[key] is not None:
681
679
  merged[key] = profile[key]
@@ -695,10 +693,6 @@ class Session:
695
693
  self.client.timeout = merged["timeout"]
696
694
  self.reasoning_effort = merged["reasoning_effort"]
697
695
  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")
702
696
  return True, f"switched to {name} ({self.model})"
703
697
 
704
698
  # ------------------------------------------------------------------
@@ -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
- ``llm.context_window`` first, then substring match over
48
- CONTEXT_WINDOWS (first match wins, case-insensitive), then
49
- DEFAULT_CONTEXT_WINDOW.
47
+ ``context_windows`` overrides (config.json) first, then substring
48
+ match over CONTEXT_WINDOWS (first match wins, case-insensitive),
49
+ then DEFAULT_CONTEXT_WINDOW.
50
50
 
51
51
  Args:
52
52
  model: The model ID to look up.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-agent-harness
3
- Version: 1.5.4.6
3
+ Version: 1.5.4.7
4
4
  Summary: A lightweight, hackable mini-OpenCode written in Python.
5
5
  Author: huming
6
6
  License-Expression: MIT
@@ -138,15 +138,17 @@ 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",
142
- "context_window": 128000
141
+ "model": "deepseek-chat"
143
142
  },
144
143
  "qwen": {
145
144
  "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
146
- "model": "qwen3.5-coder",
147
- "context_window": 131072
145
+ "model": "qwen3.5-coder"
148
146
  }
149
147
  },
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
+ },
150
152
  "subagent_llm": {
151
153
  "profile": null,
152
154
  "base_url": null,
@@ -156,8 +158,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
156
158
  "max_tokens": null,
157
159
  "timeout": null,
158
160
  "reasoning_effort": null,
159
- "stream": null,
160
- "context_window": null
161
+ "stream": null
161
162
  },
162
163
  "paths": {
163
164
  "context_path": null,
@@ -185,9 +186,10 @@ All LLM settings live in a single JSON configuration file. Environment variables
185
186
 
186
187
  ### Configuration options
187
188
 
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.
189
+ - **`llm`** — main LLM configuration. Optional keys include `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
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.
191
193
  - **`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.
192
194
  - **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
193
195
  - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).