python-agent-harness 1.5.4.1__tar.gz → 1.5.4.2__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.1/python_agent_harness.egg-info → python_agent_harness-1.5.4.2}/PKG-INFO +4 -4
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/README.md +3 -3
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/pyproject.toml +1 -1
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/client.py +11 -15
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/config.py +22 -33
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/context_manager.py +6 -6
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/session.py +3 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/token_estimator.py +9 -3
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2/python_agent_harness.egg-info}/PKG-INFO +4 -4
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_client.py +47 -17
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_config.py +39 -8
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_session.py +31 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_token_estimator.py +14 -1
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/LICENSE +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/agent.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/cli.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/commands.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/persistence.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/planmode.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/edit.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/glob.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/grep.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/insert.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tui/commands.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness.egg-info/SOURCES.txt +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/setup.cfg +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_agents_md.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_cli.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_commands.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_concurrent_subagents.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_filesystem.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_invariants.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_mcp.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_persistence.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_planmode.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_prompts.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_scenarios.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_subagent.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_subagent_isolation.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_todos_scope.py +0 -0
- {python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_tools_misc.py +0 -0
{python_agent_harness-1.5.4.1/python_agent_harness.egg-info → python_agent_harness-1.5.4.2}/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.2
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
5
|
Author: huming
|
|
6
6
|
License-Expression: MIT
|
|
@@ -146,8 +146,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
146
146
|
}
|
|
147
147
|
},
|
|
148
148
|
"context_windows": {
|
|
149
|
-
"_comment": "Optional per-model context-window overrides (tokens). Keys are model names or
|
|
150
|
-
"deepseek-v4
|
|
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
151
|
},
|
|
152
152
|
"subagent_llm": {
|
|
153
153
|
"profile": null,
|
|
@@ -188,7 +188,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
188
188
|
|
|
189
189
|
- **`llm`** — main LLM configuration. Optional keys include `backend`, `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
|
|
190
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
|
|
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
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.
|
|
193
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.
|
|
194
194
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
@@ -123,8 +123,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
"context_windows": {
|
|
126
|
-
"_comment": "Optional per-model context-window overrides (tokens). Keys are model names or
|
|
127
|
-
"deepseek-v4
|
|
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
128
|
},
|
|
129
129
|
"subagent_llm": {
|
|
130
130
|
"profile": null,
|
|
@@ -165,7 +165,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
165
165
|
|
|
166
166
|
- **`llm`** — main LLM configuration. Optional keys include `backend`, `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
|
|
167
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
|
|
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
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.
|
|
170
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.
|
|
171
171
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/client.py
RENAMED
|
@@ -200,7 +200,7 @@ class Client:
|
|
|
200
200
|
) -> None:
|
|
201
201
|
self.base_url = (base_url or config.DEFAULT_BASE_URL).rstrip("/")
|
|
202
202
|
self.api_key = api_key or _default_api_key()
|
|
203
|
-
self.model = model or config.DEFAULT_MODEL
|
|
203
|
+
self.model: str = model or config.DEFAULT_MODEL
|
|
204
204
|
self.timeout = timeout
|
|
205
205
|
self.verify = verify if verify is not None else _resolve_ca_bundle()
|
|
206
206
|
self.retry_max = config.API_RETRY_MAX if retry_max is None else retry_max
|
|
@@ -212,8 +212,6 @@ class Client:
|
|
|
212
212
|
)
|
|
213
213
|
self._config_path = config_path
|
|
214
214
|
self._http = httpx.Client(timeout=timeout, verify=self.verify)
|
|
215
|
-
# Resolve context window for this model (API discovery + fallbacks)
|
|
216
|
-
self._context_window = None # lazy-loaded
|
|
217
215
|
# True while the in-flight request was aborted (Ctrl-C): a
|
|
218
216
|
# connection error on an aborted request must NOT be retried —
|
|
219
217
|
# the user asked to stop. Cleared at the start of each chat()
|
|
@@ -233,19 +231,17 @@ class Client:
|
|
|
233
231
|
|
|
234
232
|
Resolution order: config-file ``context_windows`` overrides
|
|
235
233
|
(via ``config.get_context_window_for_model``) -> CONTEXT_WINDOWS
|
|
236
|
-
pattern match -> DEFAULT_CONTEXT_WINDOW.
|
|
237
|
-
|
|
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.
|
|
238
238
|
"""
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
# a malformed context_windows section must not break
|
|
246
|
-
# the loop: cache the safe default
|
|
247
|
-
self._context_window = config.DEFAULT_CONTEXT_WINDOW
|
|
248
|
-
return self._context_window
|
|
239
|
+
try:
|
|
240
|
+
return config.get_context_window_for_model(self.model, config_path=self._config_path)
|
|
241
|
+
except Exception:
|
|
242
|
+
# a malformed context_windows section must not break the
|
|
243
|
+
# loop: use the safe default, retry on the next access
|
|
244
|
+
return config.DEFAULT_CONTEXT_WINDOW
|
|
249
245
|
|
|
250
246
|
def close(self) -> None:
|
|
251
247
|
self._http.close()
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/config.py
RENAMED
|
@@ -5,7 +5,6 @@ Mirrors the defcustom defaults of the Emacs gptel-agent-harness.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
import fnmatch
|
|
9
8
|
import json
|
|
10
9
|
import os
|
|
11
10
|
from pathlib import Path
|
|
@@ -16,32 +15,22 @@ from .mcp.config import MCPConfig
|
|
|
16
15
|
CONTEXT_TRIGGER = 0.70
|
|
17
16
|
|
|
18
17
|
# Entries are matched in order (first match wins): put more specific
|
|
19
|
-
# patterns before general ones.
|
|
20
|
-
# on explicit entries preserves prefix matching for suffixed model
|
|
21
|
-
# names (e.g. "deepseek-v4-flash" -> deepseek-v4*), matching the
|
|
22
|
-
# legacy substring behavior.
|
|
18
|
+
# patterns before general ones.
|
|
23
19
|
CONTEXT_WINDOWS: list[tuple[str, int]] = [
|
|
24
|
-
("gpt-5-mini
|
|
25
|
-
("gpt-5
|
|
26
|
-
("gpt-oss-120b
|
|
27
|
-
("claude
|
|
28
|
-
("deepseek-v3
|
|
29
|
-
("deepseek-v4
|
|
30
|
-
("qwen3.5
|
|
31
|
-
("qwen3.6
|
|
32
|
-
("qwen3.8
|
|
33
|
-
("qwen3
|
|
34
|
-
("glm-5.2
|
|
35
|
-
("glm-5.1
|
|
36
|
-
("kimi-k2.7
|
|
37
|
-
("kimi
|
|
38
|
-
# Wildcard fallbacks for unknown models
|
|
39
|
-
("gpt-*", 128_000),
|
|
40
|
-
("claude-*", 200_000),
|
|
41
|
-
("deepseek-*", 128_000),
|
|
42
|
-
("qwen-*", 128_000),
|
|
43
|
-
("glm-*", 128_000),
|
|
44
|
-
("kimi-*", 128_000),
|
|
20
|
+
("gpt-5-mini", 128_000),
|
|
21
|
+
("gpt-5", 400_000),
|
|
22
|
+
("gpt-oss-120b", 128_000),
|
|
23
|
+
("claude", 200_000),
|
|
24
|
+
("deepseek-v3", 128_000),
|
|
25
|
+
("deepseek-v4", 1_000_000),
|
|
26
|
+
("qwen3.5", 131_072),
|
|
27
|
+
("qwen3.6", 262_144),
|
|
28
|
+
("qwen3.8", 262_144),
|
|
29
|
+
("qwen3", 131_072),
|
|
30
|
+
("glm-5.2", 1_000_000),
|
|
31
|
+
("glm-5.1", 128_000),
|
|
32
|
+
("kimi-k2.7", 256_000),
|
|
33
|
+
("kimi", 128_000),
|
|
45
34
|
]
|
|
46
35
|
DEFAULT_CONTEXT_WINDOW = 128_000
|
|
47
36
|
|
|
@@ -233,8 +222,8 @@ CONFIG_TEMPLATE = """\
|
|
|
233
222
|
}}
|
|
234
223
|
}},
|
|
235
224
|
"context_windows": {{
|
|
236
|
-
"_comment": "Optional per-model context-window overrides (tokens). Keys are model names or
|
|
237
|
-
"deepseek-v4
|
|
225
|
+
"_comment": "Optional per-model context-window overrides (tokens). Keys are model names or substrings (e.g. deepseek-v4 = 1000000); matched in file order, first match wins. Overrides the built-in CONTEXT_WINDOWS table in config.py. Remove this section to use the built-in table.",
|
|
226
|
+
"deepseek-v4": 1000000
|
|
238
227
|
}},
|
|
239
228
|
"subagent_llm": {{
|
|
240
229
|
"_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).",
|
|
@@ -476,7 +465,7 @@ def mask_secret(value: str | None) -> str:
|
|
|
476
465
|
|
|
477
466
|
|
|
478
467
|
def _match_context_window(model: str) -> int | None:
|
|
479
|
-
"""Match model name against CONTEXT_WINDOWS
|
|
468
|
+
"""Match model name against CONTEXT_WINDOWS entries (substring match).
|
|
480
469
|
|
|
481
470
|
Args:
|
|
482
471
|
model: The model ID to match (e.g., "gpt-5-mini", "claude-3-opus")
|
|
@@ -486,7 +475,7 @@ def _match_context_window(model: str) -> int | None:
|
|
|
486
475
|
"""
|
|
487
476
|
lowered = model.lower()
|
|
488
477
|
for pattern, size in CONTEXT_WINDOWS:
|
|
489
|
-
if
|
|
478
|
+
if pattern.lower() in lowered:
|
|
490
479
|
return size
|
|
491
480
|
return None
|
|
492
481
|
|
|
@@ -497,7 +486,7 @@ def load_context_windows_config(
|
|
|
497
486
|
"""Load per-model context-window overrides from the config file.
|
|
498
487
|
|
|
499
488
|
Reads the ``context_windows`` object: a mapping of model names or
|
|
500
|
-
|
|
489
|
+
substrings (matched in file order, first match wins) to token
|
|
501
490
|
counts. Keys starting with ``_`` are comments and skipped. A
|
|
502
491
|
missing file, missing section, or unreadable JSON yields ``[]``
|
|
503
492
|
(callers fall back to the built-in table); a malformed section or
|
|
@@ -530,7 +519,7 @@ def get_context_window_for_model(
|
|
|
530
519
|
the built-in table, then the default.
|
|
531
520
|
|
|
532
521
|
The config file's ``context_windows`` object (user overrides) is
|
|
533
|
-
consulted first (
|
|
522
|
+
consulted first (substring match over its keys, first match wins,
|
|
534
523
|
case-insensitive); then the CONTEXT_WINDOWS table in config.py;
|
|
535
524
|
then DEFAULT_CONTEXT_WINDOW.
|
|
536
525
|
|
|
@@ -544,7 +533,7 @@ def get_context_window_for_model(
|
|
|
544
533
|
"""
|
|
545
534
|
lowered = model.lower()
|
|
546
535
|
for pattern, size in load_context_windows_config(config_path):
|
|
547
|
-
if
|
|
536
|
+
if pattern.lower() in lowered:
|
|
548
537
|
return size
|
|
549
538
|
matched = _match_context_window(model)
|
|
550
539
|
if matched is not None:
|
|
@@ -21,9 +21,9 @@ class ContextManager:
|
|
|
21
21
|
from the loop's delegate so the call site keeps resolving them
|
|
22
22
|
through the ``agent`` module namespace (tests patch
|
|
23
23
|
``python_agent_harness.agent.estimate_payload_tokens``). The
|
|
24
|
-
context window comes from the session's client (
|
|
25
|
-
|
|
26
|
-
clients without the property.
|
|
24
|
+
context window comes from the session's client (config-file aware,
|
|
25
|
+
resolved per access); ``context_window_for`` is only the fallback
|
|
26
|
+
for clients without the property.
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
def __init__(self, loop: Any) -> None:
|
|
@@ -42,9 +42,9 @@ class ContextManager:
|
|
|
42
42
|
)
|
|
43
43
|
loop.session.calibrator.last_raw_estimate = raw
|
|
44
44
|
calibrated = loop.session.calibrator.calibrate(raw)
|
|
45
|
-
# Prefer the client's
|
|
46
|
-
#
|
|
47
|
-
# clients without the property (test doubles).
|
|
45
|
+
# Prefer the client's window (config overrides -> patterns ->
|
|
46
|
+
# default, resolved per access); fall back to the static
|
|
47
|
+
# resolver for clients without the property (test doubles).
|
|
48
48
|
client = loop.session.client
|
|
49
49
|
window = getattr(client, "context_window", None)
|
|
50
50
|
if window is None:
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/session.py
RENAMED
|
@@ -687,6 +687,9 @@ class Session:
|
|
|
687
687
|
self.client.model = merged["model"]
|
|
688
688
|
self.model = merged["model"]
|
|
689
689
|
self.store.model = merged["model"]
|
|
690
|
+
# the calibration factor is tokenizer-specific: a factor tuned
|
|
691
|
+
# to the previous model must not skew estimates for the new one
|
|
692
|
+
self.calibrator.reset()
|
|
690
693
|
self.backend = merged["backend"]
|
|
691
694
|
self.store.backend = merged["backend"]
|
|
692
695
|
self.temperature = merged["temperature"]
|
|
@@ -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
|
|
48
|
-
over 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.
|
|
@@ -68,6 +68,12 @@ class TokenCalibrator:
|
|
|
68
68
|
self.factor = 1.0
|
|
69
69
|
self.last_raw_estimate: int | None = None
|
|
70
70
|
|
|
71
|
+
def reset(self) -> None:
|
|
72
|
+
"""Drop the calibration factor (model switch: the old factor
|
|
73
|
+
was tuned to the previous model's tokenizer)."""
|
|
74
|
+
self.factor = 1.0
|
|
75
|
+
self.last_raw_estimate = None
|
|
76
|
+
|
|
71
77
|
def update(self, actual_input: int | None) -> None:
|
|
72
78
|
raw = self.last_raw_estimate
|
|
73
79
|
if actual_input is None or actual_input <= 0 or raw is None or raw <= 0:
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2/python_agent_harness.egg-info}/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.2
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
5
|
Author: huming
|
|
6
6
|
License-Expression: MIT
|
|
@@ -146,8 +146,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
146
146
|
}
|
|
147
147
|
},
|
|
148
148
|
"context_windows": {
|
|
149
|
-
"_comment": "Optional per-model context-window overrides (tokens). Keys are model names or
|
|
150
|
-
"deepseek-v4
|
|
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
151
|
},
|
|
152
152
|
"subagent_llm": {
|
|
153
153
|
"profile": null,
|
|
@@ -188,7 +188,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
188
188
|
|
|
189
189
|
- **`llm`** — main LLM configuration. Optional keys include `backend`, `temperature`, `max_tokens`, `timeout`, `reasoning_effort`, and `stream`. Values such as `reasoning_effort` are passed to the API as-is when set. `run --no-stream` overrides `stream`.
|
|
190
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
|
|
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
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.
|
|
193
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.
|
|
194
194
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
@@ -1694,36 +1694,49 @@ class TestAuthRefreshOn401(unittest.TestCase):
|
|
|
1694
1694
|
|
|
1695
1695
|
class TestContextWindow(unittest.TestCase):
|
|
1696
1696
|
"""Client.context_window: config-file overrides -> CONTEXT_WINDOWS
|
|
1697
|
-
patterns -> DEFAULT_CONTEXT_WINDOW.
|
|
1698
|
-
|
|
1697
|
+
patterns -> DEFAULT_CONTEXT_WINDOW. Resolved on every access (no
|
|
1698
|
+
caching), so model switches and config edits take effect at once."""
|
|
1699
1699
|
|
|
1700
1700
|
def _client(self, model: str, config_path: str | None = None) -> Client:
|
|
1701
1701
|
c = Client(base_url="http://x/v1", api_key="k", model=model, config_path=config_path)
|
|
1702
1702
|
self.addCleanup(c.close)
|
|
1703
1703
|
return c
|
|
1704
1704
|
|
|
1705
|
+
def test_model_change_re_resolves_window(self):
|
|
1706
|
+
"""Changing client.model (runtime /model switch) resolves the
|
|
1707
|
+
new model's window on the next access."""
|
|
1708
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1709
|
+
p = Path(d) / "config.json"
|
|
1710
|
+
p.write_text('{"context_windows": {"fake": 999999}}', encoding="utf-8")
|
|
1711
|
+
c = self._client(model="fake", config_path=str(p))
|
|
1712
|
+
self.assertEqual(c.context_window, 999_999)
|
|
1713
|
+
c.model = "gpt-5-mini"
|
|
1714
|
+
self.assertEqual(c.context_window, 128_000)
|
|
1715
|
+
c.model = "deepseek-v4-flash"
|
|
1716
|
+
self.assertEqual(c.context_window, 1_000_000)
|
|
1717
|
+
|
|
1705
1718
|
def test_config_file_override_wins(self):
|
|
1706
1719
|
"""A context_windows entry in the config file beats the
|
|
1707
|
-
built-in table, and
|
|
1720
|
+
built-in table, and edits are picked up on the next access."""
|
|
1708
1721
|
with tempfile.TemporaryDirectory() as d:
|
|
1709
1722
|
p = Path(d) / "config.json"
|
|
1710
|
-
p.write_text('{"context_windows": {"fake
|
|
1723
|
+
p.write_text('{"context_windows": {"fake": 999999}}', encoding="utf-8")
|
|
1711
1724
|
c = self._client(model="fake", config_path=str(p))
|
|
1712
1725
|
self.assertEqual(c.context_window, 999_999)
|
|
1713
|
-
#
|
|
1714
|
-
p.write_text('{"context_windows": {"fake
|
|
1715
|
-
self.assertEqual(c.context_window,
|
|
1726
|
+
# no caching: a later config change takes effect immediately
|
|
1727
|
+
p.write_text('{"context_windows": {"fake": 111111}}', encoding="utf-8")
|
|
1728
|
+
self.assertEqual(c.context_window, 111_111)
|
|
1716
1729
|
|
|
1717
|
-
def
|
|
1718
|
-
"""Config-file
|
|
1730
|
+
def test_config_file_substring_matching(self):
|
|
1731
|
+
"""Config-file keys are matched as substrings, first match
|
|
1719
1732
|
wins in file order."""
|
|
1720
1733
|
with tempfile.TemporaryDirectory() as d:
|
|
1721
1734
|
p = Path(d) / "config.json"
|
|
1722
1735
|
p.write_text(
|
|
1723
|
-
'{"context_windows": {"gpt-4
|
|
1736
|
+
'{"context_windows": {"gpt-4": 500000, "gpt-4-turbo": 300000}}',
|
|
1724
1737
|
encoding="utf-8",
|
|
1725
1738
|
)
|
|
1726
|
-
# "gpt-4-turbo" hits the FIRST entry ("gpt-4
|
|
1739
|
+
# "gpt-4-turbo" hits the FIRST entry ("gpt-4")
|
|
1727
1740
|
self.assertEqual(self._client("gpt-4-turbo", str(p)).context_window, 500_000)
|
|
1728
1741
|
self.assertEqual(self._client("gpt-4o", str(p)).context_window, 500_000)
|
|
1729
1742
|
|
|
@@ -1732,19 +1745,19 @@ class TestContextWindow(unittest.TestCase):
|
|
|
1732
1745
|
built-in CONTEXT_WINDOWS table."""
|
|
1733
1746
|
with tempfile.TemporaryDirectory() as d:
|
|
1734
1747
|
p = Path(d) / "config.json"
|
|
1735
|
-
p.write_text('{"context_windows": {"deepseek-v4
|
|
1748
|
+
p.write_text('{"context_windows": {"deepseek-v4": 1000000}}', encoding="utf-8")
|
|
1736
1749
|
self.assertEqual(self._client("gpt-5-mini", str(p)).context_window, 128_000)
|
|
1737
1750
|
self.assertEqual(self._client("deepseek-v4-flash", str(p)).context_window, 1_000_000)
|
|
1738
1751
|
|
|
1739
1752
|
def test_fallback_to_pattern_match(self):
|
|
1740
|
-
"""No config-file overrides -> CONTEXT_WINDOWS
|
|
1753
|
+
"""No config-file overrides -> CONTEXT_WINDOWS substring match."""
|
|
1741
1754
|
with tempfile.TemporaryDirectory() as d:
|
|
1742
1755
|
p = Path(d) / "config.json"
|
|
1743
1756
|
p.write_text('{"llm": {"model": "x"}}', encoding="utf-8")
|
|
1744
1757
|
self.assertEqual(self._client("gpt-4-turbo", str(p)).context_window, 128_000)
|
|
1745
1758
|
|
|
1746
1759
|
def test_fallback_to_default(self):
|
|
1747
|
-
"""No config-file overrides and no
|
|
1760
|
+
"""No config-file overrides and no table match ->
|
|
1748
1761
|
DEFAULT_CONTEXT_WINDOW."""
|
|
1749
1762
|
with tempfile.TemporaryDirectory() as d:
|
|
1750
1763
|
p = Path(d) / "config.json"
|
|
@@ -1755,19 +1768,36 @@ class TestContextWindow(unittest.TestCase):
|
|
|
1755
1768
|
)
|
|
1756
1769
|
|
|
1757
1770
|
def test_missing_config_file_uses_table(self):
|
|
1758
|
-
"""No config file at all -> CONTEXT_WINDOWS
|
|
1771
|
+
"""No config file at all -> CONTEXT_WINDOWS table."""
|
|
1759
1772
|
self.assertEqual(
|
|
1760
1773
|
self._client("deepseek-v4-flash", "/no/such/config.json").context_window,
|
|
1761
1774
|
1_000_000,
|
|
1762
1775
|
)
|
|
1763
1776
|
|
|
1777
|
+
def test_provider_prefixed_model_name(self):
|
|
1778
|
+
"""Provider-prefixed names (e.g. ZhipuAI/GLM-5.2) resolve the
|
|
1779
|
+
right window via case-insensitive substring match."""
|
|
1780
|
+
self.assertEqual(
|
|
1781
|
+
self._client("ZhipuAI/GLM-5.2", "/no/such/config.json").context_window,
|
|
1782
|
+
1_000_000,
|
|
1783
|
+
)
|
|
1784
|
+
self.assertEqual(
|
|
1785
|
+
self._client("ZhipuAI/GLM-5.1", "/no/such/config.json").context_window,
|
|
1786
|
+
128_000,
|
|
1787
|
+
)
|
|
1788
|
+
|
|
1764
1789
|
def test_malformed_config_file_falls_back_to_default(self):
|
|
1765
1790
|
"""A broken context_windows section must not break the loop:
|
|
1766
|
-
the client
|
|
1791
|
+
the client uses DEFAULT_CONTEXT_WINDOW for that access, and
|
|
1792
|
+
recovers once the file is fixed (failures are not cached)."""
|
|
1767
1793
|
with tempfile.TemporaryDirectory() as d:
|
|
1768
1794
|
p = Path(d) / "config.json"
|
|
1769
1795
|
p.write_text('{"context_windows": {"fake": "not-a-number"}}', encoding="utf-8")
|
|
1770
|
-
self.
|
|
1796
|
+
c = self._client("fake", str(p))
|
|
1797
|
+
self.assertEqual(c.context_window, 128_000)
|
|
1798
|
+
# no caching of the failure: a fixed file is picked up
|
|
1799
|
+
p.write_text('{"context_windows": {"fake": 999999}}', encoding="utf-8")
|
|
1800
|
+
self.assertEqual(c.context_window, 999_999)
|
|
1771
1801
|
|
|
1772
1802
|
|
|
1773
1803
|
if __name__ == "__main__":
|
|
@@ -218,22 +218,22 @@ class TestContextWindowsConfig(unittest.TestCase):
|
|
|
218
218
|
with tempfile.TemporaryDirectory() as d:
|
|
219
219
|
p = Path(d) / "config.json"
|
|
220
220
|
p.write_text(
|
|
221
|
-
'{"context_windows": {"deepseek-v4
|
|
221
|
+
'{"context_windows": {"deepseek-v4": 1000000, "gpt-5": 400000}}',
|
|
222
222
|
encoding="utf-8",
|
|
223
223
|
)
|
|
224
224
|
self.assertEqual(
|
|
225
225
|
config.load_context_windows_config(p),
|
|
226
|
-
[("deepseek-v4
|
|
226
|
+
[("deepseek-v4", 1000000), ("gpt-5", 400000)],
|
|
227
227
|
)
|
|
228
228
|
|
|
229
229
|
def test_comment_keys_skipped(self):
|
|
230
230
|
with tempfile.TemporaryDirectory() as d:
|
|
231
231
|
p = Path(d) / "config.json"
|
|
232
232
|
p.write_text(
|
|
233
|
-
'{"context_windows": {"_comment": "hi", "kimi
|
|
233
|
+
'{"context_windows": {"_comment": "hi", "kimi": 256000}}',
|
|
234
234
|
encoding="utf-8",
|
|
235
235
|
)
|
|
236
|
-
self.assertEqual(config.load_context_windows_config(p), [("kimi
|
|
236
|
+
self.assertEqual(config.load_context_windows_config(p), [("kimi", 256000)])
|
|
237
237
|
|
|
238
238
|
def test_section_must_be_object(self):
|
|
239
239
|
with tempfile.TemporaryDirectory() as d:
|
|
@@ -245,7 +245,7 @@ class TestContextWindowsConfig(unittest.TestCase):
|
|
|
245
245
|
def test_size_must_be_integer(self):
|
|
246
246
|
with tempfile.TemporaryDirectory() as d:
|
|
247
247
|
p = Path(d) / "config.json"
|
|
248
|
-
p.write_text('{"context_windows": {"m
|
|
248
|
+
p.write_text('{"context_windows": {"m": "big"}}', encoding="utf-8")
|
|
249
249
|
with self.assertRaises(ValueError):
|
|
250
250
|
config.load_context_windows_config(p)
|
|
251
251
|
|
|
@@ -253,7 +253,7 @@ class TestContextWindowsConfig(unittest.TestCase):
|
|
|
253
253
|
"""True is an int subclass but not a valid token count."""
|
|
254
254
|
with tempfile.TemporaryDirectory() as d:
|
|
255
255
|
p = Path(d) / "config.json"
|
|
256
|
-
p.write_text('{"context_windows": {"m
|
|
256
|
+
p.write_text('{"context_windows": {"m": true}}', encoding="utf-8")
|
|
257
257
|
with self.assertRaises(ValueError):
|
|
258
258
|
config.load_context_windows_config(p)
|
|
259
259
|
|
|
@@ -262,7 +262,7 @@ class TestContextWindowsConfig(unittest.TestCase):
|
|
|
262
262
|
with tempfile.TemporaryDirectory() as d:
|
|
263
263
|
p = Path(d) / "config.json"
|
|
264
264
|
p.write_text(
|
|
265
|
-
'{"context_windows": {"deepseek-v4
|
|
265
|
+
'{"context_windows": {"deepseek-v4": 1000000, "gpt-4-turbo": 300000}}',
|
|
266
266
|
encoding="utf-8",
|
|
267
267
|
)
|
|
268
268
|
self.assertEqual(config.get_context_window_for_model("deepseek-v4-flash", p), 1000000)
|
|
@@ -280,9 +280,40 @@ class TestContextWindowsConfig(unittest.TestCase):
|
|
|
280
280
|
def test_get_context_window_case_insensitive(self):
|
|
281
281
|
with tempfile.TemporaryDirectory() as d:
|
|
282
282
|
p = Path(d) / "config.json"
|
|
283
|
-
p.write_text('{"context_windows": {"DeepSeek-V4
|
|
283
|
+
p.write_text('{"context_windows": {"DeepSeek-V4": 1000000}}', encoding="utf-8")
|
|
284
284
|
self.assertEqual(config.get_context_window_for_model("deepseek-v4-flash", p), 1000000)
|
|
285
285
|
|
|
286
|
+
def test_get_context_window_provider_prefixed_name(self):
|
|
287
|
+
"""Provider-prefixed model names (e.g. "ZhipuAI/GLM-5.2") match
|
|
288
|
+
their table entry via case-insensitive substring match."""
|
|
289
|
+
# provider prefix + uppercase org, model name in the middle
|
|
290
|
+
self.assertEqual(
|
|
291
|
+
config.get_context_window_for_model("ZhipuAI/GLM-5.2", "/no/such/config.json"),
|
|
292
|
+
1_000_000,
|
|
293
|
+
)
|
|
294
|
+
# lowercased provider prefix matches too
|
|
295
|
+
self.assertEqual(
|
|
296
|
+
config.get_context_window_for_model("zhipuai/glm-5.2", "/no/such/config.json"),
|
|
297
|
+
1_000_000,
|
|
298
|
+
)
|
|
299
|
+
# plain model name still matches
|
|
300
|
+
self.assertEqual(
|
|
301
|
+
config.get_context_window_for_model("glm-5.2", "/no/such/config.json"),
|
|
302
|
+
1_000_000,
|
|
303
|
+
)
|
|
304
|
+
# a different minor version must NOT hit the glm-5.2 entry
|
|
305
|
+
self.assertEqual(
|
|
306
|
+
config.get_context_window_for_model("ZhipuAI/GLM-5.3", "/no/such/config.json"),
|
|
307
|
+
config.DEFAULT_CONTEXT_WINDOW,
|
|
308
|
+
)
|
|
309
|
+
# provider prefix must not break other families either
|
|
310
|
+
self.assertEqual(
|
|
311
|
+
config.get_context_window_for_model(
|
|
312
|
+
"DeepSeek/deepseek-v4-flash", "/no/such/config.json"
|
|
313
|
+
),
|
|
314
|
+
1_000_000,
|
|
315
|
+
)
|
|
316
|
+
|
|
286
317
|
def test_get_context_window_no_file(self):
|
|
287
318
|
self.assertEqual(
|
|
288
319
|
config.get_context_window_for_model("deepseek-v4", "/no/such/file.json"),
|
|
@@ -808,6 +808,37 @@ class TestModelSwitching(unittest.TestCase):
|
|
|
808
808
|
self.assertIn("deepseek", msg)
|
|
809
809
|
self.assertIn("glm", msg)
|
|
810
810
|
|
|
811
|
+
def test_switch_model_re_resolves_context_window(self):
|
|
812
|
+
"""Switching models must make the next context-window access
|
|
813
|
+
resolve for the NEW model: the ratio computation divides by
|
|
814
|
+
the new model's window, not the old one."""
|
|
815
|
+
from python_agent_harness.client import Client
|
|
816
|
+
|
|
817
|
+
with tempfile.TemporaryDirectory() as d:
|
|
818
|
+
p = os.path.join(d, "config.json")
|
|
819
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
820
|
+
f.write('{"context_windows": {"deepseek-v4": 1000000}}')
|
|
821
|
+
client = Client(base_url="http://x/v1", api_key="k", model="gpt-5-mini", config_path=p)
|
|
822
|
+
self.addCleanup(client.close)
|
|
823
|
+
session = RecordingSession(model_profiles={"deepseek": {"model": "deepseek-v4-flash"}})
|
|
824
|
+
session.client = client
|
|
825
|
+
self.assertEqual(client.context_window, 128_000)
|
|
826
|
+
success, _ = session.switch_model("deepseek")
|
|
827
|
+
self.assertTrue(success)
|
|
828
|
+
self.assertEqual(client.context_window, 1_000_000)
|
|
829
|
+
|
|
830
|
+
def test_switch_model_resets_calibrator(self):
|
|
831
|
+
"""Switching models must drop the token-calibration factor: it
|
|
832
|
+
was tuned to the previous model's tokenizer and would skew the
|
|
833
|
+
first context estimates for the new model."""
|
|
834
|
+
session = RecordingSession(model_profiles={"deepseek": {"model": "deepseek-v4-flash"}})
|
|
835
|
+
session.calibrator.factor = 2.5
|
|
836
|
+
session.calibrator.last_raw_estimate = 1234
|
|
837
|
+
success, _ = session.switch_model("deepseek")
|
|
838
|
+
self.assertTrue(success)
|
|
839
|
+
self.assertEqual(session.calibrator.factor, 1.0)
|
|
840
|
+
self.assertIsNone(session.calibrator.last_raw_estimate)
|
|
841
|
+
|
|
811
842
|
def test_switch_model_preserves_conversation_history(self):
|
|
812
843
|
"""Switching models does not clear conversation history."""
|
|
813
844
|
from python_agent_harness.models import Message
|
|
@@ -55,7 +55,7 @@ class TestTokenizer(unittest.TestCase):
|
|
|
55
55
|
built-in table (matched before CONTEXT_WINDOWS)."""
|
|
56
56
|
with tempfile.TemporaryDirectory() as d:
|
|
57
57
|
p = Path(d) / "config.json"
|
|
58
|
-
p.write_text('{"context_windows": {"deepseek-v4
|
|
58
|
+
p.write_text('{"context_windows": {"deepseek-v4": 2000000}}', encoding="utf-8")
|
|
59
59
|
self.assertEqual(context_window_for("deepseek-v4-flash", str(p)), 2_000_000)
|
|
60
60
|
# models the override doesn't cover still use the table
|
|
61
61
|
self.assertEqual(context_window_for("gpt-5-mini", str(p)), 128_000)
|
|
@@ -77,6 +77,19 @@ class TestTokenizer(unittest.TestCase):
|
|
|
77
77
|
c2.update(500)
|
|
78
78
|
self.assertEqual(c2.factor, 1.0)
|
|
79
79
|
|
|
80
|
+
def test_calibrator_reset(self):
|
|
81
|
+
"""reset() drops the factor and raw estimate: after a model
|
|
82
|
+
switch the next estimate is uncalibrated (identity), and the
|
|
83
|
+
stale factor tuned to the old tokenizer is gone."""
|
|
84
|
+
c = TokenCalibrator()
|
|
85
|
+
c.last_raw_estimate = 1000
|
|
86
|
+
c.update(3000)
|
|
87
|
+
self.assertEqual(c.factor, 3.0)
|
|
88
|
+
c.reset()
|
|
89
|
+
self.assertEqual(c.factor, 1.0)
|
|
90
|
+
self.assertIsNone(c.last_raw_estimate)
|
|
91
|
+
self.assertEqual(c.calibrate(1000), 1000)
|
|
92
|
+
|
|
80
93
|
def test_payload_tokens(self):
|
|
81
94
|
msgs = [
|
|
82
95
|
{"role": "user", "content": "hello"},
|
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/diffrender.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/mcp/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/mcp/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/mcp/config.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/mcp/manager.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/models.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/persistence.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/planmode.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/agent.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/plan.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/review.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts/title.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/prompts.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/subagent.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tool_runner.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/base.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/bash.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/edit.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/glob.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/grep.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/insert.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/mcp.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/mkdir.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/planexit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/question.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/read.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/skill.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/todo.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tools/write.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tui/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tui/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tui/core.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tui/input.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/python_agent_harness/tui/render.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_concurrent_subagents.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.1 → python_agent_harness-1.5.4.2}/tests/test_subagent_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|