python-agent-harness 1.5.4.6__tar.gz → 1.5.4.8__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.
- {python_agent_harness-1.5.4.6/python_agent_harness.egg-info → python_agent_harness-1.5.4.8}/PKG-INFO +12 -10
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/README.md +11 -9
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/pyproject.toml +1 -1
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/cli.py +9 -3
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/client.py +47 -31
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/config.py +58 -87
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/planmode.py +3 -1
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/session.py +1 -7
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/token_estimator.py +3 -3
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/edit.py +1 -1
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/glob.py +2 -2
- python_agent_harness-1.5.4.8/python_agent_harness/tools/glob_mac.py +115 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/grep.py +13 -1
- python_agent_harness-1.5.4.8/python_agent_harness/tools/grep_mac.py +209 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/insert.py +2 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/write.py +3 -1
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8/python_agent_harness.egg-info}/PKG-INFO +12 -10
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_client.py +69 -45
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_config.py +90 -138
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_filesystem.py +1 -1
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_session.py +2 -6
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_token_estimator.py +6 -6
- python_agent_harness-1.5.4.6/python_agent_harness/tools/glob_mac.py +0 -97
- python_agent_harness-1.5.4.6/python_agent_harness/tools/grep_mac.py +0 -92
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/LICENSE +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/agent.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/commands.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/context_manager.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/persistence.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/diffapply.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/commands.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness.egg-info/SOURCES.txt +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/setup.cfg +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_agents_md.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_cli.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_commands.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_concurrent_subagents.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_invariants.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_mcp.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_persistence.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_planmode.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_prompts.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_scenarios.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_subagent.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_subagent_isolation.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_todos_scope.py +0 -0
- {python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/tests/test_tools_misc.py +0 -0
{python_agent_harness-1.5.4.6/python_agent_harness.egg-info → python_agent_harness-1.5.4.8}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.4.
|
|
3
|
+
Version: 1.5.4.8
|
|
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`,
|
|
189
|
-
- **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`.
|
|
190
|
-
- **`
|
|
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`,
|
|
166
|
-
- **`models`** — named LLM profiles for runtime switching with `/model`. A profile is a partial settings dictionary; unset keys inherit from the main `llm`.
|
|
167
|
-
- **`
|
|
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`).
|
|
@@ -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
|
-
|
|
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
|
|
{python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/client.py
RENAMED
|
@@ -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:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
-
|
|
262
|
+
return Client(
|
|
270
263
|
base_url=self.base_url,
|
|
271
264
|
api_key=self.api_key,
|
|
272
265
|
model=self.model,
|
|
@@ -278,20 +271,22 @@ 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).
|
|
286
277
|
|
|
287
278
|
A blocked ``iter_lines()`` read must be interrupted so the agent
|
|
288
279
|
loop can stop promptly. Closing the pool alone is NOT enough:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
the
|
|
294
|
-
|
|
280
|
+
``httpx.Client.close()`` from another thread cannot force-close a
|
|
281
|
+
socket whose fd is parked in a kernel ``recv``. So we first
|
|
282
|
+
reach into every in-flight connection socket and both
|
|
283
|
+
``shutdown(SHUT_RDWR)`` it (wakes the blocked read on Linux) and
|
|
284
|
+
``close()`` the raw fd (the reliable wake on macOS/BSD, where
|
|
285
|
+
``shutdown`` alone may not rouse a parked ``recv``); the woken
|
|
286
|
+
read surfaces as a connection error the loop treats as a cancel.
|
|
287
|
+
We then close the pool and swap in a fresh client for the next
|
|
288
|
+
request. See ``_abort_inflight_sockets`` for the platform
|
|
289
|
+
details.
|
|
295
290
|
"""
|
|
296
291
|
self._aborted = True
|
|
297
292
|
old = self._http
|
|
@@ -828,15 +823,31 @@ def _default_api_key() -> str | None:
|
|
|
828
823
|
|
|
829
824
|
|
|
830
825
|
def _abort_inflight_sockets(client: httpx.Client) -> None:
|
|
831
|
-
"""Wake any blocked stream reads
|
|
826
|
+
"""Wake any blocked stream reads on every live pool socket.
|
|
832
827
|
|
|
833
828
|
Reaches through httpx/httpcore internals (transport -> pool ->
|
|
834
|
-
connection -> network stream -> raw socket) and
|
|
835
|
-
``shutdown(SHUT_RDWR)``
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
829
|
+
connection -> network stream -> raw socket) and, for each live
|
|
830
|
+
connection, both ``shutdown(SHUT_RDWR)`` AND ``close()`` the raw
|
|
831
|
+
socket.
|
|
832
|
+
|
|
833
|
+
Both steps are needed for cross-platform reliability when another
|
|
834
|
+
thread is already blocked in ``recv``:
|
|
835
|
+
|
|
836
|
+
- On Linux, ``shutdown(SHUT_RDWR)`` reliably wakes the blocked
|
|
837
|
+
``recv`` (turning it into a clean EOF / connection error), while
|
|
838
|
+
httpx's own ``close()`` cannot — it can't force-close a socket
|
|
839
|
+
whose fd is parked in a kernel read.
|
|
840
|
+
- On macOS/BSD (notably CI's macos-latest on py3.11), ``shutdown``
|
|
841
|
+
from another thread is NOT guaranteed to wake a ``recv`` already
|
|
842
|
+
parked in the kernel; the read stays blocked until the fd itself
|
|
843
|
+
is closed. ``socket.close()`` closes the fd directly and does
|
|
844
|
+
wake it, so we always follow the shutdown with an explicit
|
|
845
|
+
close.
|
|
846
|
+
|
|
847
|
+
Closing the fd out from under httpcore is safe here: ``abort()``
|
|
848
|
+
swaps in a fresh pool immediately and closes the old one under
|
|
849
|
+
``contextlib.suppress`` right after this call, so a double-close is
|
|
850
|
+
harmless. Every step is best-effort; any failure is ignored.
|
|
840
851
|
"""
|
|
841
852
|
pool = getattr(getattr(client, "_transport", None), "_pool", None)
|
|
842
853
|
for conn in getattr(pool, "_connections", None) or ():
|
|
@@ -846,8 +857,13 @@ def _abort_inflight_sockets(client: httpx.Client) -> None:
|
|
|
846
857
|
except Exception: # noqa: BLE001 - best effort
|
|
847
858
|
sock = None
|
|
848
859
|
if sock is not None:
|
|
860
|
+
# shutdown first (wakes the recv on Linux without touching
|
|
861
|
+
# the fd), then close the fd (the reliable wake on
|
|
862
|
+
# macOS/BSD, where shutdown alone may not).
|
|
849
863
|
with contextlib.suppress(OSError):
|
|
850
864
|
sock.shutdown(_socket.SHUT_RDWR)
|
|
865
|
+
with contextlib.suppress(OSError):
|
|
866
|
+
sock.close()
|
|
851
867
|
|
|
852
868
|
|
|
853
869
|
def _iter_sse(lines: Iterator[str]) -> Iterator[str]:
|
{python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/config.py
RENAMED
|
@@ -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,
|
|
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
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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(
|
|
489
|
+
data = _read_config(path)
|
|
529
490
|
except ValueError:
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if isinstance(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
544
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
{python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/planmode.py
RENAMED
|
@@ -112,4 +112,6 @@ class PlanMode:
|
|
|
112
112
|
return self.mode == AgentMode.PLAN
|
|
113
113
|
|
|
114
114
|
def plan_reminder(self) -> str:
|
|
115
|
-
return config.PLAN_MODE_SUBAGENT_REMINDER
|
|
115
|
+
return config.PLAN_MODE_SUBAGENT_REMINDER.replace(
|
|
116
|
+
"%s", self.plan_file or self.plan_file_path()
|
|
117
|
+
)
|
{python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/session.py
RENAMED
|
@@ -444,7 +444,7 @@ class Session:
|
|
|
444
444
|
)
|
|
445
445
|
if approved:
|
|
446
446
|
self.switch_to_build()
|
|
447
|
-
msg = config.PLAN_EXIT_APPROVED_MESSAGE
|
|
447
|
+
msg = config.PLAN_EXIT_APPROVED_MESSAGE.replace("%s", self.plan_mode.plan_file or "")
|
|
448
448
|
self.pending_user_prompts.append(msg)
|
|
449
449
|
return (
|
|
450
450
|
"User approved switching to build agent. You are now in "
|
|
@@ -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
|
-
``
|
|
48
|
-
CONTEXT_WINDOWS (first match wins, case-insensitive),
|
|
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.
|
{python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/edit.py
RENAMED
|
@@ -90,7 +90,7 @@ class Edit(Tool):
|
|
|
90
90
|
if old is None:
|
|
91
91
|
return "Error: old_str is required for non-diff edits"
|
|
92
92
|
try:
|
|
93
|
-
with open(path, encoding="utf-8") as f:
|
|
93
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
94
94
|
content = f.read()
|
|
95
95
|
except OSError as e:
|
|
96
96
|
return f"Error: cannot read {path}: {e}"
|
{python_agent_harness-1.5.4.6 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/glob.py
RENAMED
|
@@ -107,7 +107,7 @@ class GlobTool(Tool):
|
|
|
107
107
|
return f"Error: {e}"
|
|
108
108
|
if proc.returncode != 0:
|
|
109
109
|
# Failure banner is prepended to whatever git emitted.
|
|
110
|
-
banner = f"Glob failed with exit code {proc.returncode}\
|
|
110
|
+
banner = f"Glob failed with exit code {proc.returncode}\nSTDOUT:\n\n"
|
|
111
111
|
return _spool(banner + (proc.stdout or "") + (proc.stderr or ""), "glob")
|
|
112
112
|
return _git_glob_results(proc.stdout, git_root, base, depth)
|
|
113
113
|
|
|
@@ -141,7 +141,7 @@ class GlobTool(Tool):
|
|
|
141
141
|
return f"Error: {e}"
|
|
142
142
|
out = proc.stdout
|
|
143
143
|
if proc.returncode != 0:
|
|
144
|
-
out = f"Glob failed with exit code {proc.returncode}\
|
|
144
|
+
out = f"Glob failed with exit code {proc.returncode}\nSTDOUT:\n\n" + out
|
|
145
145
|
return _spool(out, "glob")
|
|
146
146
|
|
|
147
147
|
|