python-agent-harness 1.5.4.5__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.5/python_agent_harness.egg-info → python_agent_harness-1.5.4.6}/PKG-INFO +10 -12
  2. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/README.md +9 -11
  3. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/pyproject.toml +1 -1
  4. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/__init__.py +1 -1
  5. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/cli.py +3 -9
  6. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/client.py +18 -9
  7. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/config.py +87 -58
  8. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/session.py +6 -0
  9. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/token_estimator.py +3 -3
  10. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6/python_agent_harness.egg-info}/PKG-INFO +10 -12
  11. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_client.py +43 -52
  12. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_config.py +138 -90
  13. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_session.py +6 -2
  14. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_token_estimator.py +6 -6
  15. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/LICENSE +0 -0
  16. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/__main__.py +0 -0
  17. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/agent.py +0 -0
  18. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/commands.py +0 -0
  19. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/context_manager.py +0 -0
  20. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/diffrender.py +0 -0
  21. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/mcp/__init__.py +0 -0
  22. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/mcp/client.py +0 -0
  23. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/mcp/config.py +0 -0
  24. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/mcp/manager.py +0 -0
  25. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/models.py +0 -0
  26. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/persistence.py +0 -0
  27. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/planmode.py +0 -0
  28. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/agent.md +0 -0
  29. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/build-switch.md +0 -0
  30. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/commands/explain.md +0 -0
  31. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/compact.md +0 -0
  32. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/initialize.md +0 -0
  33. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/plan-mode.md +0 -0
  34. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/plan.md +0 -0
  35. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/review.md +0 -0
  36. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/subagent.md +0 -0
  37. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/summary.md +0 -0
  38. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/task-completion-rules.md +0 -0
  39. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts/title.md +0 -0
  40. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/prompts.py +0 -0
  41. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/subagent.py +0 -0
  42. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tool_runner.py +0 -0
  43. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/__init__.py +0 -0
  44. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/agent_tool.py +0 -0
  45. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/base.py +0 -0
  46. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/bash.py +0 -0
  47. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/diffapply.py +0 -0
  48. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/edit.py +0 -0
  49. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/edit_mac.py +0 -0
  50. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/filesystem.py +0 -0
  51. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/glob.py +0 -0
  52. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/glob_mac.py +0 -0
  53. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/grep.py +0 -0
  54. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/grep_mac.py +0 -0
  55. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/insert.py +0 -0
  56. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/mcp.py +0 -0
  57. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/mkdir.py +0 -0
  58. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/planexit.py +0 -0
  59. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/question.py +0 -0
  60. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/read.py +0 -0
  61. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/skill.py +0 -0
  62. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/todo.py +0 -0
  63. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tools/write.py +0 -0
  64. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tui/__init__.py +0 -0
  65. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tui/commands.py +0 -0
  66. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tui/core.py +0 -0
  67. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tui/input.py +0 -0
  68. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness/tui/render.py +0 -0
  69. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness.egg-info/SOURCES.txt +0 -0
  70. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness.egg-info/dependency_links.txt +0 -0
  71. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness.egg-info/entry_points.txt +0 -0
  72. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness.egg-info/requires.txt +0 -0
  73. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/python_agent_harness.egg-info/top_level.txt +0 -0
  74. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/setup.cfg +0 -0
  75. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_agents_md.py +0 -0
  76. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_cli.py +0 -0
  77. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_commands.py +0 -0
  78. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_concurrent_subagents.py +0 -0
  79. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_context_rules.py +0 -0
  80. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_diffrender.py +0 -0
  81. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_filesystem.py +0 -0
  82. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_invariants.py +0 -0
  83. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_mcp.py +0 -0
  84. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_models.py +0 -0
  85. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_persistence.py +0 -0
  86. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_planmode.py +0 -0
  87. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_prompts.py +0 -0
  88. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_scenarios.py +0 -0
  89. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_subagent.py +0 -0
  90. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_subagent_isolation.py +0 -0
  91. {python_agent_harness-1.5.4.5 → python_agent_harness-1.5.4.6}/tests/test_todos_scope.py +0 -0
  92. {python_agent_harness-1.5.4.5 → 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.5
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,10 +185,9 @@ 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 `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
193
  - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
@@ -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,10 +162,9 @@ 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 `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
170
  - **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.5"
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.5"
8
+ __version__ = "1.5.4.6"
9
9
 
10
10
  __all__ = [
11
11
  "Session",
@@ -217,17 +217,11 @@ 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
- 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}")
221
223
  else:
222
224
  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)")
231
225
  return 0
232
226
 
233
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
@@ -173,6 +174,7 @@ DEFAULT_LLM: dict = {
173
174
  "timeout": 600.0,
174
175
  "reasoning_effort": None,
175
176
  "stream": True,
177
+ "context_window": None,
176
178
  }
177
179
 
178
180
  DEFAULT_PATHS: dict = {
@@ -196,6 +198,7 @@ DEFAULT_SUBAGENT_LLM: dict = {
196
198
  "timeout": None,
197
199
  "reasoning_effort": None,
198
200
  "stream": None,
201
+ "context_window": None,
199
202
  }
200
203
 
201
204
  CONFIG_TEMPLATE = """\
@@ -208,20 +211,18 @@ CONFIG_TEMPLATE = """\
208
211
  "stream": true
209
212
  }},
210
213
  "models": {{
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.",
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.",
212
215
  "deepseek": {{
213
216
  "base_url": "https://api.deepseek.com/v1",
214
- "model": "deepseek-chat"
217
+ "model": "deepseek-chat",
218
+ "context_window": 128000
215
219
  }},
216
220
  "openai": {{
217
221
  "base_url": "https://api.openai.com/v1",
218
- "model": "gpt-5-mini"
222
+ "model": "gpt-5-mini",
223
+ "context_window": 128000
219
224
  }}
220
225
  }},
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
- }},
225
226
  "subagent_llm": {{
226
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).",
227
228
  "profile": null,
@@ -232,7 +233,8 @@ CONFIG_TEMPLATE = """\
232
233
  "max_tokens": null,
233
234
  "timeout": null,
234
235
  "reasoning_effort": null,
235
- "stream": null
236
+ "stream": null,
237
+ "context_window": null
236
238
  }},
237
239
  "paths": {{
238
240
  "_comment": "Optional overrides for context and skill directories. Absolute paths or ~ expansion supported.",
@@ -270,6 +272,22 @@ _SUBAGENT_ENV_OVERRIDES = {
270
272
  }
271
273
 
272
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
+
273
291
  def _config_path(path: str | os.PathLike | None = None) -> Path:
274
292
  """Resolve the config file path: explicit arg > $PYTHON_AGENT_HARNESS_CONFIG > default."""
275
293
  if path:
@@ -305,6 +323,9 @@ def load_llm_config(path: str | os.PathLike | None = None) -> dict:
305
323
 
306
324
  The config file is JSON with an ``llm`` object (see `CONFIG_TEMPLATE`).
307
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".
308
329
  """
309
330
  settings = dict(DEFAULT_LLM)
310
331
  data = _read_config(path)
@@ -320,9 +341,19 @@ def load_llm_config(path: str | os.PathLike | None = None) -> dict:
320
341
  "timeout",
321
342
  "reasoning_effort",
322
343
  "stream",
344
+ "context_window",
323
345
  ):
324
346
  if key in llm and llm[key] is not None:
325
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
+ )
326
357
  for key, env in _ENV_OVERRIDES.items():
327
358
  val = os.environ.get(env)
328
359
  if val:
@@ -378,6 +409,7 @@ def load_subagent_llm_config(
378
409
  "timeout",
379
410
  "reasoning_effort",
380
411
  "stream",
412
+ "context_window",
381
413
  ):
382
414
  if key in profile and profile[key] is not None:
383
415
  overrides[key] = profile[key]
@@ -473,65 +505,62 @@ def _match_context_window(model: str) -> int | None:
473
505
  return None
474
506
 
475
507
 
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.
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``).
487
525
  """
526
+ # Try the llm.context_window from the config file first.
488
527
  try:
489
- data = _read_config(path)
528
+ data = _read_config(config_path)
490
529
  except ValueError:
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
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
509
541
 
510
542
 
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.
543
+ def resolve_context_window(settings: dict, model: str | None = None) -> int:
544
+ """Resolve the context window from a fully-merged settings dict.
517
545
 
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.
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.
522
552
 
523
553
  Args:
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.
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.
530
559
  """
531
- lowered = model.lower()
532
- for pattern, size in load_context_windows_config(config_path):
533
- if pattern.lower() in lowered:
534
- 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 ""
535
564
  matched = _match_context_window(model)
536
565
  if matched is not None:
537
566
  return matched
@@ -661,6 +661,7 @@ 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),
664
665
  }
665
666
  for key, val in current.items():
666
667
  merged.setdefault(key, val)
@@ -674,6 +675,7 @@ class Session:
674
675
  "timeout",
675
676
  "reasoning_effort",
676
677
  "stream",
678
+ "context_window",
677
679
  ):
678
680
  if key in profile and profile[key] is not None:
679
681
  merged[key] = profile[key]
@@ -693,6 +695,10 @@ class Session:
693
695
  self.client.timeout = merged["timeout"]
694
696
  self.reasoning_effort = merged["reasoning_effort"]
695
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")
696
702
  return True, f"switched to {name} ({self.model})"
697
703
 
698
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-agent-harness
3
- Version: 1.5.4.5
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,10 +185,9 @@ 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 `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
193
  - **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).