python-agent-harness 1.5.3__tar.gz → 1.5.4.1__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.3/python_agent_harness.egg-info → python_agent_harness-1.5.4.1}/PKG-INFO +8 -1
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/README.md +5 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/pyproject.toml +6 -1
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/cli.py +8 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/client.py +23 -2
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/commands.py +1 -2
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/config.py +108 -20
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/context_manager.py +11 -2
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/mcp/client.py +2 -4
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/mcp/manager.py +0 -2
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/persistence.py +1 -2
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/token_estimator.py +12 -8
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1/python_agent_harness.egg-info}/PKG-INFO +8 -1
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_client.py +81 -1
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_config.py +112 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_token_estimator.py +31 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/LICENSE +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/agent.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/planmode.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/session.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/edit.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/glob.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/grep.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/insert.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tui/commands.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness.egg-info/SOURCES.txt +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/setup.cfg +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_agents_md.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_cli.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_commands.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_concurrent_subagents.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_filesystem.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_invariants.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_mcp.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_persistence.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_planmode.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_prompts.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_scenarios.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_session.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_subagent.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_subagent_isolation.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_todos_scope.py +0 -0
- {python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_tools_misc.py +0 -0
{python_agent_harness-1.5.3/python_agent_harness.egg-info → python_agent_harness-1.5.4.1}/PKG-INFO
RENAMED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.4.1
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
|
+
Author: huming
|
|
6
|
+
License-Expression: MIT
|
|
5
7
|
Requires-Python: >=3.11
|
|
6
8
|
Description-Content-Type: text/markdown
|
|
7
9
|
License-File: LICENSE
|
|
@@ -143,6 +145,10 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
143
145
|
"model": "qwen3.5-coder"
|
|
144
146
|
}
|
|
145
147
|
},
|
|
148
|
+
"context_windows": {
|
|
149
|
+
"_comment": "Optional per-model context-window overrides (tokens). Keys are model names or fnmatch patterns (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
|
+
},
|
|
146
152
|
"subagent_llm": {
|
|
147
153
|
"profile": null,
|
|
148
154
|
"base_url": null,
|
|
@@ -182,6 +188,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
182
188
|
|
|
183
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`.
|
|
184
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 fnmatch patterns (e.g., `deepseek-v4*`); matched in file order, first match wins. Overrides the built-in `CONTEXT_WINDOWS` table in `config.py`.
|
|
185
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.
|
|
186
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.
|
|
187
194
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
@@ -122,6 +122,10 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
122
122
|
"model": "qwen3.5-coder"
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
|
+
"context_windows": {
|
|
126
|
+
"_comment": "Optional per-model context-window overrides (tokens). Keys are model names or fnmatch patterns (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
|
+
},
|
|
125
129
|
"subagent_llm": {
|
|
126
130
|
"profile": null,
|
|
127
131
|
"base_url": null,
|
|
@@ -161,6 +165,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
161
165
|
|
|
162
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`.
|
|
163
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 fnmatch patterns (e.g., `deepseek-v4*`); matched in file order, first match wins. Overrides the built-in `CONTEXT_WINDOWS` table in `config.py`.
|
|
164
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
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.
|
|
166
171
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
@@ -4,10 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-agent-harness"
|
|
7
|
-
version = "1.5.
|
|
7
|
+
version = "1.5.4.1"
|
|
8
8
|
description = "A lightweight, hackable mini-OpenCode written in Python."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "huming" }
|
|
13
|
+
]
|
|
14
|
+
license = 'MIT'
|
|
15
|
+
license-files = ["LICENSE"]
|
|
11
16
|
dependencies = [
|
|
12
17
|
"rich>=13.0",
|
|
13
18
|
"httpx>=0.27",
|
|
@@ -221,6 +221,14 @@ def cmd_config(args: argparse.Namespace) -> int:
|
|
|
221
221
|
print(f" {name}: model={model_name}, base_url={base_url}")
|
|
222
222
|
else:
|
|
223
223
|
print("models: (none configured — add a 'models' section to use /model)")
|
|
224
|
+
# Show context-window overrides
|
|
225
|
+
context_windows = config.load_context_windows_config(args.path)
|
|
226
|
+
if context_windows:
|
|
227
|
+
print("context_windows:")
|
|
228
|
+
for pattern, size in context_windows:
|
|
229
|
+
print(f" {pattern}: {size}")
|
|
230
|
+
else:
|
|
231
|
+
print("context_windows: (none configured — built-in table in config.py applies)")
|
|
224
232
|
return 0
|
|
225
233
|
|
|
226
234
|
|
|
@@ -11,6 +11,7 @@ import contextlib
|
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
13
|
import random
|
|
14
|
+
import socket as _socket
|
|
14
15
|
import threading
|
|
15
16
|
import time
|
|
16
17
|
import uuid
|
|
@@ -211,6 +212,8 @@ class Client:
|
|
|
211
212
|
)
|
|
212
213
|
self._config_path = config_path
|
|
213
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
|
|
214
217
|
# True while the in-flight request was aborted (Ctrl-C): a
|
|
215
218
|
# connection error on an aborted request must NOT be retried —
|
|
216
219
|
# the user asked to stop. Cleared at the start of each chat()
|
|
@@ -224,6 +227,26 @@ class Client:
|
|
|
224
227
|
else (_llm_log_path() if config.LLM_LOG_ENABLED else None)
|
|
225
228
|
)
|
|
226
229
|
|
|
230
|
+
@property
|
|
231
|
+
def context_window(self) -> int:
|
|
232
|
+
"""Get the context window for this model.
|
|
233
|
+
|
|
234
|
+
Resolution order: config-file ``context_windows`` overrides
|
|
235
|
+
(via ``config.get_context_window_for_model``) -> CONTEXT_WINDOWS
|
|
236
|
+
pattern match -> DEFAULT_CONTEXT_WINDOW. The resolved value is
|
|
237
|
+
cached for the life of the client.
|
|
238
|
+
"""
|
|
239
|
+
if self._context_window is None:
|
|
240
|
+
try:
|
|
241
|
+
self._context_window = config.get_context_window_for_model(
|
|
242
|
+
self.model, config_path=self._config_path
|
|
243
|
+
)
|
|
244
|
+
except Exception:
|
|
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
|
|
249
|
+
|
|
227
250
|
def close(self) -> None:
|
|
228
251
|
self._http.close()
|
|
229
252
|
|
|
@@ -810,8 +833,6 @@ def _abort_inflight_sockets(client: httpx.Client) -> None:
|
|
|
810
833
|
down an idle socket is harmless (the pool is closed right after
|
|
811
834
|
anyway); any failure is ignored (best effort).
|
|
812
835
|
"""
|
|
813
|
-
import socket as _socket
|
|
814
|
-
|
|
815
836
|
pool = getattr(getattr(client, "_transport", None), "_pool", None)
|
|
816
837
|
for conn in getattr(pool, "_connections", None) or ():
|
|
817
838
|
try:
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/commands.py
RENAMED
|
@@ -17,6 +17,7 @@ Tool availability per command:
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import re
|
|
20
|
+
import subprocess
|
|
20
21
|
from collections.abc import Callable
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
from typing import Any
|
|
@@ -35,8 +36,6 @@ def _substitute(text: str, path: str, extra: str | None) -> str:
|
|
|
35
36
|
|
|
36
37
|
def _project_root(cwd: str) -> str:
|
|
37
38
|
"""Best-effort project root (git dir or parent with AGENTS.md)."""
|
|
38
|
-
import subprocess
|
|
39
|
-
|
|
40
39
|
d = Path(cwd).resolve()
|
|
41
40
|
try:
|
|
42
41
|
out = subprocess.run(
|
|
@@ -5,34 +5,43 @@ Mirrors the defcustom defaults of the Emacs gptel-agent-harness.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import fnmatch
|
|
8
9
|
import json
|
|
9
10
|
import os
|
|
10
11
|
from pathlib import Path
|
|
11
|
-
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
from .mcp.config import MCPConfig
|
|
13
|
+
from .mcp.config import MCPConfig
|
|
15
14
|
|
|
16
15
|
# ---- context management -------------------------------------------------
|
|
17
16
|
CONTEXT_TRIGGER = 0.70
|
|
18
17
|
|
|
19
18
|
# Entries are matched in order (first match wins): put more specific
|
|
20
|
-
# patterns before general ones.
|
|
19
|
+
# patterns before general ones. Supports wildcards (*). Trailing `*`
|
|
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.
|
|
21
23
|
CONTEXT_WINDOWS: list[tuple[str, int]] = [
|
|
22
|
-
("gpt-5-mini", 128_000),
|
|
23
|
-
("gpt-5", 400_000),
|
|
24
|
-
("gpt-oss-120b", 128_000),
|
|
25
|
-
("claude", 200_000),
|
|
26
|
-
("deepseek-v3", 128_000),
|
|
27
|
-
("deepseek-v4", 1_000_000),
|
|
28
|
-
("qwen3.5", 131_072),
|
|
29
|
-
("qwen3.6", 262_144),
|
|
30
|
-
("qwen3.8", 262_144),
|
|
31
|
-
("qwen3", 131_072),
|
|
32
|
-
("glm-5.2", 1_000_000),
|
|
33
|
-
("glm-5.1", 128_000),
|
|
34
|
-
("kimi-k2.7", 256_000),
|
|
35
|
-
("kimi", 128_000),
|
|
24
|
+
("gpt-5-mini*", 128_000),
|
|
25
|
+
("gpt-5*", 400_000),
|
|
26
|
+
("gpt-oss-120b*", 128_000),
|
|
27
|
+
("claude*", 200_000),
|
|
28
|
+
("deepseek-v3*", 128_000),
|
|
29
|
+
("deepseek-v4*", 1_000_000),
|
|
30
|
+
("qwen3.5*", 131_072),
|
|
31
|
+
("qwen3.6*", 262_144),
|
|
32
|
+
("qwen3.8*", 262_144),
|
|
33
|
+
("qwen3*", 131_072),
|
|
34
|
+
("glm-5.2*", 1_000_000),
|
|
35
|
+
("glm-5.1*", 128_000),
|
|
36
|
+
("kimi-k2.7*", 256_000),
|
|
37
|
+
("kimi*", 128_000),
|
|
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),
|
|
36
45
|
]
|
|
37
46
|
DEFAULT_CONTEXT_WINDOW = 128_000
|
|
38
47
|
|
|
@@ -223,6 +232,10 @@ CONFIG_TEMPLATE = """\
|
|
|
223
232
|
"model": "gpt-5-mini"
|
|
224
233
|
}}
|
|
225
234
|
}},
|
|
235
|
+
"context_windows": {{
|
|
236
|
+
"_comment": "Optional per-model context-window overrides (tokens). Keys are model names or fnmatch patterns (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.",
|
|
237
|
+
"deepseek-v4*": 1000000
|
|
238
|
+
}},
|
|
226
239
|
"subagent_llm": {{
|
|
227
240
|
"_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
241
|
"profile": null,
|
|
@@ -428,8 +441,6 @@ def load_mcp_config(path: str | os.PathLike | None = None) -> MCPConfig:
|
|
|
428
441
|
``mcp`` SDK is only needed when servers are actually configured and
|
|
429
442
|
connected — reading the config never requires it.
|
|
430
443
|
"""
|
|
431
|
-
from .mcp.config import MCPConfig
|
|
432
|
-
|
|
433
444
|
data = _read_config(path)
|
|
434
445
|
section = data.get("mcp") or {}
|
|
435
446
|
if not isinstance(section, dict):
|
|
@@ -462,3 +473,80 @@ def load_models_config(path: str | os.PathLike | None = None) -> dict[str, dict]
|
|
|
462
473
|
|
|
463
474
|
def mask_secret(value: str | None) -> str:
|
|
464
475
|
return "****" if value else "(unset)"
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _match_context_window(model: str) -> int | None:
|
|
479
|
+
"""Match model name against CONTEXT_WINDOWS patterns (supports wildcards).
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
model: The model ID to match (e.g., "gpt-5-mini", "claude-3-opus")
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
The context window size if matched, None otherwise
|
|
486
|
+
"""
|
|
487
|
+
lowered = model.lower()
|
|
488
|
+
for pattern, size in CONTEXT_WINDOWS:
|
|
489
|
+
if fnmatch.fnmatch(lowered, pattern.lower()):
|
|
490
|
+
return size
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def load_context_windows_config(
|
|
495
|
+
path: str | os.PathLike | None = None,
|
|
496
|
+
) -> list[tuple[str, int]]:
|
|
497
|
+
"""Load per-model context-window overrides from the config file.
|
|
498
|
+
|
|
499
|
+
Reads the ``context_windows`` object: a mapping of model names or
|
|
500
|
+
fnmatch patterns (matched in file order, first match wins) to token
|
|
501
|
+
counts. Keys starting with ``_`` are comments and skipped. A
|
|
502
|
+
missing file, missing section, or unreadable JSON yields ``[]``
|
|
503
|
+
(callers fall back to the built-in table); a malformed section or
|
|
504
|
+
non-integer size raises ValueError so config errors surface.
|
|
505
|
+
"""
|
|
506
|
+
try:
|
|
507
|
+
data = _read_config(path)
|
|
508
|
+
except ValueError:
|
|
509
|
+
return []
|
|
510
|
+
section = data.get("context_windows") or {}
|
|
511
|
+
if not isinstance(section, dict):
|
|
512
|
+
raise ValueError(f"config file {_config_path(path)}: context_windows must be an object")
|
|
513
|
+
entries: list[tuple[str, int]] = []
|
|
514
|
+
for pattern, size in section.items():
|
|
515
|
+
if pattern.startswith("_"):
|
|
516
|
+
continue
|
|
517
|
+
if isinstance(size, bool) or not isinstance(size, int):
|
|
518
|
+
raise ValueError(
|
|
519
|
+
f"config file {_config_path(path)}: context_windows.{pattern} must be an integer"
|
|
520
|
+
)
|
|
521
|
+
entries.append((pattern, size))
|
|
522
|
+
return entries
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def get_context_window_for_model(
|
|
526
|
+
model: str,
|
|
527
|
+
config_path: str | os.PathLike | None = None,
|
|
528
|
+
) -> int:
|
|
529
|
+
"""Get the context window for MODEL: config-file overrides, then
|
|
530
|
+
the built-in table, then the default.
|
|
531
|
+
|
|
532
|
+
The config file's ``context_windows`` object (user overrides) is
|
|
533
|
+
consulted first (fnmatch over its patterns, first match wins,
|
|
534
|
+
case-insensitive); then the CONTEXT_WINDOWS table in config.py;
|
|
535
|
+
then DEFAULT_CONTEXT_WINDOW.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
model: The model ID to look up (e.g. "deepseek-v4-flash")
|
|
539
|
+
config_path: Optional path to the config file; defaults to the
|
|
540
|
+
standard config location (config.json).
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
The context window size as an integer.
|
|
544
|
+
"""
|
|
545
|
+
lowered = model.lower()
|
|
546
|
+
for pattern, size in load_context_windows_config(config_path):
|
|
547
|
+
if fnmatch.fnmatch(lowered, pattern.lower()):
|
|
548
|
+
return size
|
|
549
|
+
matched = _match_context_window(model)
|
|
550
|
+
if matched is not None:
|
|
551
|
+
return matched
|
|
552
|
+
return DEFAULT_CONTEXT_WINDOW
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/context_manager.py
RENAMED
|
@@ -20,7 +20,10 @@ class ContextManager:
|
|
|
20
20
|
``update_context_ratio`` receives the two token-estimator functions
|
|
21
21
|
from the loop's delegate so the call site keeps resolving them
|
|
22
22
|
through the ``agent`` module namespace (tests patch
|
|
23
|
-
``python_agent_harness.agent.estimate_payload_tokens``).
|
|
23
|
+
``python_agent_harness.agent.estimate_payload_tokens``). The
|
|
24
|
+
context window comes from the session's client (cached, config-
|
|
25
|
+
file aware); ``context_window_for`` is only the fallback for
|
|
26
|
+
clients without the property.
|
|
24
27
|
"""
|
|
25
28
|
|
|
26
29
|
def __init__(self, loop: Any) -> None:
|
|
@@ -39,7 +42,13 @@ class ContextManager:
|
|
|
39
42
|
)
|
|
40
43
|
loop.session.calibrator.last_raw_estimate = raw
|
|
41
44
|
calibrated = loop.session.calibrator.calibrate(raw)
|
|
42
|
-
window
|
|
45
|
+
# Prefer the client's cached window (config overrides ->
|
|
46
|
+
# patterns -> default); fall back to the static resolver for
|
|
47
|
+
# clients without the property (test doubles).
|
|
48
|
+
client = loop.session.client
|
|
49
|
+
window = getattr(client, "context_window", None)
|
|
50
|
+
if window is None:
|
|
51
|
+
window = context_window_for(loop.session.model)
|
|
43
52
|
loop.session.context_ratio = calibrated / float(window)
|
|
44
53
|
loop.session.notify("context")
|
|
45
54
|
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/mcp/client.py
RENAMED
|
@@ -18,10 +18,12 @@ content-block dicts), ``structured_content`` and ``is_error``.
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
+
import contextlib
|
|
21
22
|
import os
|
|
22
23
|
from typing import Any
|
|
23
24
|
|
|
24
25
|
try:
|
|
26
|
+
import httpx2 # shipped as part of the mcp SDK
|
|
25
27
|
from mcp import Client
|
|
26
28
|
from mcp.client.sse import sse_client
|
|
27
29
|
from mcp.client.stdio import StdioServerParameters, stdio_client
|
|
@@ -93,8 +95,6 @@ class MCPClient:
|
|
|
93
95
|
assert cfg.url is not None
|
|
94
96
|
if cfg.transport == "streamable-http":
|
|
95
97
|
if cfg.headers:
|
|
96
|
-
import httpx2 # shipped as part of the mcp SDK
|
|
97
|
-
|
|
98
98
|
self._http_client = httpx2.AsyncClient(headers=cfg.headers)
|
|
99
99
|
return streamable_http_client(cfg.url, http_client=self._http_client)
|
|
100
100
|
return streamable_http_client(cfg.url)
|
|
@@ -117,8 +117,6 @@ class MCPClient:
|
|
|
117
117
|
|
|
118
118
|
async def close(self) -> None:
|
|
119
119
|
"""Tear down the connection (best effort, never raises)."""
|
|
120
|
-
import contextlib
|
|
121
|
-
|
|
122
120
|
client, self._client = self._client, None
|
|
123
121
|
if client is not None:
|
|
124
122
|
with contextlib.suppress(Exception): # teardown noise is not an error
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/persistence.py
RENAMED
|
@@ -12,6 +12,7 @@ Ported from gptel-agent-harness-session.el.
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import ast
|
|
15
16
|
import os
|
|
16
17
|
import re
|
|
17
18
|
import threading
|
|
@@ -284,8 +285,6 @@ class SessionPersistence:
|
|
|
284
285
|
|
|
285
286
|
def _parse_metadata_value(value: str) -> str:
|
|
286
287
|
"""Parse a repr()-style metadata value, mirroring elisp read-from-string."""
|
|
287
|
-
import ast
|
|
288
|
-
|
|
289
288
|
try:
|
|
290
289
|
parsed = ast.literal_eval(value)
|
|
291
290
|
if isinstance(parsed, str):
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/token_estimator.py
RENAMED
|
@@ -8,6 +8,7 @@ to reduce drift.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import os
|
|
11
12
|
import re
|
|
12
13
|
|
|
13
14
|
from . import config
|
|
@@ -39,17 +40,20 @@ def estimate_tokens(text: str) -> int:
|
|
|
39
40
|
return round(latin / 4.0 + cjk / 2.0)
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
def context_window_for(model: str) -> int:
|
|
43
|
+
def context_window_for(model: str, config_path: str | os.PathLike | None = None) -> int:
|
|
43
44
|
"""Return the context window for MODEL, or a safe fallback.
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
Delegates to ``config.get_context_window_for_model``: config-file
|
|
47
|
+
``context_windows`` overrides (config.json) first, then fnmatch
|
|
48
|
+
over CONTEXT_WINDOWS (first match wins, case-insensitive), then
|
|
49
|
+
DEFAULT_CONTEXT_WINDOW.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
model: The model ID to look up.
|
|
53
|
+
config_path: Optional config file path; defaults to the
|
|
54
|
+
standard config location (config.json).
|
|
47
55
|
"""
|
|
48
|
-
|
|
49
|
-
for pattern, size in config.CONTEXT_WINDOWS:
|
|
50
|
-
if pattern in lowered:
|
|
51
|
-
return size
|
|
52
|
-
return config.DEFAULT_CONTEXT_WINDOW
|
|
56
|
+
return config.get_context_window_for_model(model, config_path=config_path)
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
class TokenCalibrator:
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1/python_agent_harness.egg-info}/PKG-INFO
RENAMED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.4.1
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
|
+
Author: huming
|
|
6
|
+
License-Expression: MIT
|
|
5
7
|
Requires-Python: >=3.11
|
|
6
8
|
Description-Content-Type: text/markdown
|
|
7
9
|
License-File: LICENSE
|
|
@@ -143,6 +145,10 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
143
145
|
"model": "qwen3.5-coder"
|
|
144
146
|
}
|
|
145
147
|
},
|
|
148
|
+
"context_windows": {
|
|
149
|
+
"_comment": "Optional per-model context-window overrides (tokens). Keys are model names or fnmatch patterns (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
|
+
},
|
|
146
152
|
"subagent_llm": {
|
|
147
153
|
"profile": null,
|
|
148
154
|
"base_url": null,
|
|
@@ -182,6 +188,7 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
182
188
|
|
|
183
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`.
|
|
184
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 fnmatch patterns (e.g., `deepseek-v4*`); matched in file order, first match wins. Overrides the built-in `CONTEXT_WINDOWS` table in `config.py`.
|
|
185
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.
|
|
186
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.
|
|
187
194
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
@@ -6,6 +6,7 @@ import sys
|
|
|
6
6
|
import tempfile
|
|
7
7
|
import time
|
|
8
8
|
import unittest
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
from unittest import mock
|
|
10
11
|
|
|
11
12
|
import httpx
|
|
@@ -27,13 +28,14 @@ def make_client(
|
|
|
27
28
|
retry_max: int | None = None,
|
|
28
29
|
retry_base_delay: float | None = None,
|
|
29
30
|
retry_max_delay: float | None = None,
|
|
31
|
+
model: str = "fake",
|
|
30
32
|
) -> Client:
|
|
31
33
|
srv = serve()
|
|
32
34
|
host, port = srv.server_address
|
|
33
35
|
c = Client(
|
|
34
36
|
base_url=f"http://{host}:{port}/v1",
|
|
35
37
|
api_key="test",
|
|
36
|
-
model=
|
|
38
|
+
model=model,
|
|
37
39
|
retry_max=retry_max,
|
|
38
40
|
retry_base_delay=retry_base_delay,
|
|
39
41
|
retry_max_delay=retry_max_delay,
|
|
@@ -1690,5 +1692,83 @@ class TestAuthRefreshOn401(unittest.TestCase):
|
|
|
1690
1692
|
c.close()
|
|
1691
1693
|
|
|
1692
1694
|
|
|
1695
|
+
class TestContextWindow(unittest.TestCase):
|
|
1696
|
+
"""Client.context_window: config-file overrides -> CONTEXT_WINDOWS
|
|
1697
|
+
patterns -> DEFAULT_CONTEXT_WINDOW. The resolved value is cached
|
|
1698
|
+
for the life of the client."""
|
|
1699
|
+
|
|
1700
|
+
def _client(self, model: str, config_path: str | None = None) -> Client:
|
|
1701
|
+
c = Client(base_url="http://x/v1", api_key="k", model=model, config_path=config_path)
|
|
1702
|
+
self.addCleanup(c.close)
|
|
1703
|
+
return c
|
|
1704
|
+
|
|
1705
|
+
def test_config_file_override_wins(self):
|
|
1706
|
+
"""A context_windows entry in the config file beats the
|
|
1707
|
+
built-in table, and the resolved value is cached."""
|
|
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
|
+
# cached: a later config change must not affect the window
|
|
1714
|
+
p.write_text('{"context_windows": {"fake*": 111111}}', encoding="utf-8")
|
|
1715
|
+
self.assertEqual(c.context_window, 999_999)
|
|
1716
|
+
|
|
1717
|
+
def test_config_file_wildcard_matching(self):
|
|
1718
|
+
"""Config-file patterns support fnmatch wildcards, first match
|
|
1719
|
+
wins in file order."""
|
|
1720
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1721
|
+
p = Path(d) / "config.json"
|
|
1722
|
+
p.write_text(
|
|
1723
|
+
'{"context_windows": {"gpt-4*": 500000, "gpt-4-turbo": 300000}}',
|
|
1724
|
+
encoding="utf-8",
|
|
1725
|
+
)
|
|
1726
|
+
# "gpt-4-turbo" hits the FIRST entry ("gpt-4*")
|
|
1727
|
+
self.assertEqual(self._client("gpt-4-turbo", str(p)).context_window, 500_000)
|
|
1728
|
+
self.assertEqual(self._client("gpt-4o", str(p)).context_window, 500_000)
|
|
1729
|
+
|
|
1730
|
+
def test_unknown_model_in_config_file_falls_to_table(self):
|
|
1731
|
+
"""Config-file overrides that don't match fall through to the
|
|
1732
|
+
built-in CONTEXT_WINDOWS table."""
|
|
1733
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1734
|
+
p = Path(d) / "config.json"
|
|
1735
|
+
p.write_text('{"context_windows": {"deepseek-v4*": 1000000}}', encoding="utf-8")
|
|
1736
|
+
self.assertEqual(self._client("gpt-5-mini", str(p)).context_window, 128_000)
|
|
1737
|
+
self.assertEqual(self._client("deepseek-v4-flash", str(p)).context_window, 1_000_000)
|
|
1738
|
+
|
|
1739
|
+
def test_fallback_to_pattern_match(self):
|
|
1740
|
+
"""No config-file overrides -> CONTEXT_WINDOWS wildcard."""
|
|
1741
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1742
|
+
p = Path(d) / "config.json"
|
|
1743
|
+
p.write_text('{"llm": {"model": "x"}}', encoding="utf-8")
|
|
1744
|
+
self.assertEqual(self._client("gpt-4-turbo", str(p)).context_window, 128_000)
|
|
1745
|
+
|
|
1746
|
+
def test_fallback_to_default(self):
|
|
1747
|
+
"""No config-file overrides and no pattern match ->
|
|
1748
|
+
DEFAULT_CONTEXT_WINDOW."""
|
|
1749
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1750
|
+
p = Path(d) / "config.json"
|
|
1751
|
+
p.write_text('{"llm": {"model": "x"}}', encoding="utf-8")
|
|
1752
|
+
self.assertEqual(
|
|
1753
|
+
self._client("totally-unknown-model", str(p)).context_window,
|
|
1754
|
+
config.DEFAULT_CONTEXT_WINDOW,
|
|
1755
|
+
)
|
|
1756
|
+
|
|
1757
|
+
def test_missing_config_file_uses_table(self):
|
|
1758
|
+
"""No config file at all -> CONTEXT_WINDOWS patterns."""
|
|
1759
|
+
self.assertEqual(
|
|
1760
|
+
self._client("deepseek-v4-flash", "/no/such/config.json").context_window,
|
|
1761
|
+
1_000_000,
|
|
1762
|
+
)
|
|
1763
|
+
|
|
1764
|
+
def test_malformed_config_file_falls_back_to_default(self):
|
|
1765
|
+
"""A broken context_windows section must not break the loop:
|
|
1766
|
+
the client caches DEFAULT_CONTEXT_WINDOW."""
|
|
1767
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1768
|
+
p = Path(d) / "config.json"
|
|
1769
|
+
p.write_text('{"context_windows": {"fake": "not-a-number"}}', encoding="utf-8")
|
|
1770
|
+
self.assertEqual(self._client("fake", str(p)).context_window, 128_000)
|
|
1771
|
+
|
|
1772
|
+
|
|
1693
1773
|
if __name__ == "__main__":
|
|
1694
1774
|
unittest.main()
|
|
@@ -180,6 +180,118 @@ class TestConfigFile(unittest.TestCase):
|
|
|
180
180
|
self.assertIn("reasoning_effort", config.CONFIG_TEMPLATE)
|
|
181
181
|
self.assertIn('"stream"', config.CONFIG_TEMPLATE)
|
|
182
182
|
self.assertIn('"subagent_llm"', config.CONFIG_TEMPLATE)
|
|
183
|
+
self.assertIn('"context_windows"', config.CONFIG_TEMPLATE)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestContextWindowsConfig(unittest.TestCase):
|
|
187
|
+
"""Config-file context-window overrides: loaded from the
|
|
188
|
+
``context_windows`` object, matched before the built-in table."""
|
|
189
|
+
|
|
190
|
+
def setUp(self):
|
|
191
|
+
self._saved = {k: os.environ.get(k) for k in ENV_KEYS}
|
|
192
|
+
for k in ENV_KEYS:
|
|
193
|
+
os.environ.pop(k, None)
|
|
194
|
+
|
|
195
|
+
def tearDown(self):
|
|
196
|
+
for k, v in self._saved.items():
|
|
197
|
+
if v is None:
|
|
198
|
+
os.environ.pop(k, None)
|
|
199
|
+
else:
|
|
200
|
+
os.environ[k] = v
|
|
201
|
+
|
|
202
|
+
def test_missing_file_returns_empty(self):
|
|
203
|
+
self.assertEqual(config.load_context_windows_config("/no/such/file.json"), [])
|
|
204
|
+
|
|
205
|
+
def test_empty_section_returns_empty(self):
|
|
206
|
+
with tempfile.TemporaryDirectory() as d:
|
|
207
|
+
p = Path(d) / "config.json"
|
|
208
|
+
p.write_text('{"llm": {"model": "m"}}', encoding="utf-8")
|
|
209
|
+
self.assertEqual(config.load_context_windows_config(p), [])
|
|
210
|
+
|
|
211
|
+
def test_bad_json_returns_empty(self):
|
|
212
|
+
with tempfile.TemporaryDirectory() as d:
|
|
213
|
+
p = Path(d) / "config.json"
|
|
214
|
+
p.write_text("not {valid json", encoding="utf-8")
|
|
215
|
+
self.assertEqual(config.load_context_windows_config(p), [])
|
|
216
|
+
|
|
217
|
+
def test_overrides_loaded_in_order(self):
|
|
218
|
+
with tempfile.TemporaryDirectory() as d:
|
|
219
|
+
p = Path(d) / "config.json"
|
|
220
|
+
p.write_text(
|
|
221
|
+
'{"context_windows": {"deepseek-v4*": 1000000, "gpt-5*": 400000}}',
|
|
222
|
+
encoding="utf-8",
|
|
223
|
+
)
|
|
224
|
+
self.assertEqual(
|
|
225
|
+
config.load_context_windows_config(p),
|
|
226
|
+
[("deepseek-v4*", 1000000), ("gpt-5*", 400000)],
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def test_comment_keys_skipped(self):
|
|
230
|
+
with tempfile.TemporaryDirectory() as d:
|
|
231
|
+
p = Path(d) / "config.json"
|
|
232
|
+
p.write_text(
|
|
233
|
+
'{"context_windows": {"_comment": "hi", "kimi*": 256000}}',
|
|
234
|
+
encoding="utf-8",
|
|
235
|
+
)
|
|
236
|
+
self.assertEqual(config.load_context_windows_config(p), [("kimi*", 256000)])
|
|
237
|
+
|
|
238
|
+
def test_section_must_be_object(self):
|
|
239
|
+
with tempfile.TemporaryDirectory() as d:
|
|
240
|
+
p = Path(d) / "config.json"
|
|
241
|
+
p.write_text('{"context_windows": "nope"}', encoding="utf-8")
|
|
242
|
+
with self.assertRaises(ValueError):
|
|
243
|
+
config.load_context_windows_config(p)
|
|
244
|
+
|
|
245
|
+
def test_size_must_be_integer(self):
|
|
246
|
+
with tempfile.TemporaryDirectory() as d:
|
|
247
|
+
p = Path(d) / "config.json"
|
|
248
|
+
p.write_text('{"context_windows": {"m*": "big"}}', encoding="utf-8")
|
|
249
|
+
with self.assertRaises(ValueError):
|
|
250
|
+
config.load_context_windows_config(p)
|
|
251
|
+
|
|
252
|
+
def test_bool_size_rejected(self):
|
|
253
|
+
"""True is an int subclass but not a valid token count."""
|
|
254
|
+
with tempfile.TemporaryDirectory() as d:
|
|
255
|
+
p = Path(d) / "config.json"
|
|
256
|
+
p.write_text('{"context_windows": {"m*": true}}', encoding="utf-8")
|
|
257
|
+
with self.assertRaises(ValueError):
|
|
258
|
+
config.load_context_windows_config(p)
|
|
259
|
+
|
|
260
|
+
def test_get_context_window_precedence(self):
|
|
261
|
+
"""Config-file override -> CONTEXT_WINDOWS -> default."""
|
|
262
|
+
with tempfile.TemporaryDirectory() as d:
|
|
263
|
+
p = Path(d) / "config.json"
|
|
264
|
+
p.write_text(
|
|
265
|
+
'{"context_windows": {"deepseek-v4*": 1000000, "gpt-4-turbo": 300000}}',
|
|
266
|
+
encoding="utf-8",
|
|
267
|
+
)
|
|
268
|
+
self.assertEqual(config.get_context_window_for_model("deepseek-v4-flash", p), 1000000)
|
|
269
|
+
# config-file match beats the built-in table
|
|
270
|
+
self.assertEqual(config.get_context_window_for_model("gpt-4-turbo", p), 300000)
|
|
271
|
+
# built-in table still applies when no override matches
|
|
272
|
+
self.assertEqual(config.get_context_window_for_model("gpt-5-mini", p), 128000)
|
|
273
|
+
self.assertEqual(config.get_context_window_for_model("kimi-k2.7-0613", p), 256000)
|
|
274
|
+
# unknown model -> default
|
|
275
|
+
self.assertEqual(
|
|
276
|
+
config.get_context_window_for_model("unknown-model", p),
|
|
277
|
+
config.DEFAULT_CONTEXT_WINDOW,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def test_get_context_window_case_insensitive(self):
|
|
281
|
+
with tempfile.TemporaryDirectory() as d:
|
|
282
|
+
p = Path(d) / "config.json"
|
|
283
|
+
p.write_text('{"context_windows": {"DeepSeek-V4*": 1000000}}', encoding="utf-8")
|
|
284
|
+
self.assertEqual(config.get_context_window_for_model("deepseek-v4-flash", p), 1000000)
|
|
285
|
+
|
|
286
|
+
def test_get_context_window_no_file(self):
|
|
287
|
+
self.assertEqual(
|
|
288
|
+
config.get_context_window_for_model("deepseek-v4", "/no/such/file.json"),
|
|
289
|
+
1_000_000,
|
|
290
|
+
)
|
|
291
|
+
self.assertEqual(
|
|
292
|
+
config.get_context_window_for_model("totally-unknown", "/no/such/file.json"),
|
|
293
|
+
config.DEFAULT_CONTEXT_WINDOW,
|
|
294
|
+
)
|
|
183
295
|
|
|
184
296
|
|
|
185
297
|
class TestSubagentLlmConfig(unittest.TestCase):
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
1
3
|
import unittest
|
|
4
|
+
from pathlib import Path
|
|
2
5
|
|
|
3
6
|
from python_agent_harness.token_estimator import (
|
|
4
7
|
TokenCalibrator,
|
|
@@ -11,6 +14,18 @@ from python_agent_harness.token_estimator import (
|
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class TestTokenizer(unittest.TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
# pin the default config path to a nonexistent file so the
|
|
19
|
+
# tests never read the user's real config.json
|
|
20
|
+
self._saved_cfg = os.environ.get("PYTHON_AGENT_HARNESS_CONFIG")
|
|
21
|
+
os.environ["PYTHON_AGENT_HARNESS_CONFIG"] = "/no/such/harness-config.json"
|
|
22
|
+
|
|
23
|
+
def tearDown(self):
|
|
24
|
+
if self._saved_cfg is None:
|
|
25
|
+
os.environ.pop("PYTHON_AGENT_HARNESS_CONFIG", None)
|
|
26
|
+
else:
|
|
27
|
+
os.environ["PYTHON_AGENT_HARNESS_CONFIG"] = self._saved_cfg
|
|
28
|
+
|
|
14
29
|
def test_empty_text_zero_tokens(self):
|
|
15
30
|
self.assertEqual(estimate_tokens(""), 0)
|
|
16
31
|
|
|
@@ -26,10 +41,26 @@ class TestTokenizer(unittest.TestCase):
|
|
|
26
41
|
|
|
27
42
|
def test_context_window_matching(self):
|
|
28
43
|
self.assertEqual(context_window_for("deepseek-v4"), 1_000_000)
|
|
44
|
+
self.assertEqual(context_window_for("deepseek-v4-flash"), 1_000_000)
|
|
45
|
+
self.assertEqual(context_window_for("deepseek-v4-pro"), 1_000_000)
|
|
29
46
|
self.assertEqual(context_window_for("gpt-5-mini"), 128_000)
|
|
47
|
+
self.assertEqual(context_window_for("gpt-5-pro"), 400_000)
|
|
30
48
|
self.assertEqual(context_window_for("claude-sonnet"), 200_000)
|
|
49
|
+
self.assertEqual(context_window_for("qwen3.5-32b"), 131_072)
|
|
50
|
+
self.assertEqual(context_window_for("kimi-k2.7-0613"), 256_000)
|
|
31
51
|
self.assertEqual(context_window_for("unknown-model"), 128_000)
|
|
32
52
|
|
|
53
|
+
def test_context_window_config_file_override(self):
|
|
54
|
+
"""A context_windows section in the config file overrides the
|
|
55
|
+
built-in table (matched before CONTEXT_WINDOWS)."""
|
|
56
|
+
with tempfile.TemporaryDirectory() as d:
|
|
57
|
+
p = Path(d) / "config.json"
|
|
58
|
+
p.write_text('{"context_windows": {"deepseek-v4*": 2000000}}', encoding="utf-8")
|
|
59
|
+
self.assertEqual(context_window_for("deepseek-v4-flash", str(p)), 2_000_000)
|
|
60
|
+
# models the override doesn't cover still use the table
|
|
61
|
+
self.assertEqual(context_window_for("gpt-5-mini", str(p)), 128_000)
|
|
62
|
+
self.assertEqual(context_window_for("unknown-model", str(p)), 128_000)
|
|
63
|
+
|
|
33
64
|
def test_calibrator(self):
|
|
34
65
|
c = TokenCalibrator()
|
|
35
66
|
c.last_raw_estimate = 1000
|
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/diffrender.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/mcp/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/mcp/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/planmode.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/agent.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/compact.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/plan.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/review.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/subagent.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/summary.md
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/prompts/title.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/subagent.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tool_runner.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/agent_tool.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/base.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/bash.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/edit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/filesystem.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/glob.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/grep.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/insert.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/mcp.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/mkdir.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/planexit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/question.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/read.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/skill.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/todo.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tools/write.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tui/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tui/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tui/core.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/python_agent_harness/tui/input.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/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.3 → python_agent_harness-1.5.4.1}/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
|
|
File without changes
|
{python_agent_harness-1.5.3 → python_agent_harness-1.5.4.1}/tests/test_subagent_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|