python-agent-harness 1.5.6.3__tar.gz → 1.5.6.5__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.6.3/python_agent_harness.egg-info → python_agent_harness-1.5.6.5}/PKG-INFO +3 -4
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/README.md +2 -3
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/pyproject.toml +1 -1
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/__init__.py +1 -4
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/core/agent.py +20 -2
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/core/models.py +16 -6
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/core/token_estimator.py +4 -1
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/entry/cli.py +12 -17
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/entry/headless.py +42 -76
- python_agent_harness-1.5.6.5/python_agent_harness/entry/restore.py +149 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/entry/server.py +10 -13
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/agent.md +1 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/agents/reviewer.md +0 -2
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/session/config.py +63 -38
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/session/session.py +43 -12
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/__init__.py +1 -8
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/base.py +1 -2
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/bash.py +109 -1
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/diffapply.py +1 -1
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/edit.py +4 -1
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/filesystem.py +6 -96
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/grep.py +14 -7
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/grep_mac.py +13 -61
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tui/commands.py +25 -105
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tui/core.py +1 -1
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tui/render.py +38 -3
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5/python_agent_harness.egg-info}/PKG-INFO +3 -4
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness.egg-info/SOURCES.txt +1 -7
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/tests/test_invariants.py +16 -17
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/tests/test_scenarios.py +58 -0
- python_agent_harness-1.5.6.3/python_agent_harness/lsp/__init__.py +0 -14
- python_agent_harness-1.5.6.3/python_agent_harness/lsp/client.py +0 -493
- python_agent_harness-1.5.6.3/python_agent_harness/lsp/config.py +0 -108
- python_agent_harness-1.5.6.3/python_agent_harness/lsp/manager.py +0 -117
- python_agent_harness-1.5.6.3/python_agent_harness/tools/insert.py +0 -87
- python_agent_harness-1.5.6.3/python_agent_harness/tools/lsp.py +0 -287
- python_agent_harness-1.5.6.3/python_agent_harness/tools/mkdir.py +0 -32
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/LICENSE +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/core/__init__.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/core/context_manager.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/core/planmode.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/core/subagent.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/core/tool_runner.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/entry/__init__.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/entry/controller.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/entry/view.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/io/__init__.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/io/attachments.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/io/diffrender.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/io/persistence.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/io/text_filter.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/llm/__init__.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/llm/client.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/__init__.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/core.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/session/__init__.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/session/commands.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/glob.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/glob_mac.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/setup.cfg +0 -0
{python_agent_harness-1.5.6.3/python_agent_harness.egg-info → python_agent_harness-1.5.6.5}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.6.
|
|
3
|
+
Version: 1.5.6.5
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
5
|
Author-email: Huming Chen <chenhuming@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -74,7 +74,7 @@ Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_
|
|
|
74
74
|
- **FSM-driven execution** — explicit `WAIT` / `TOOL` / `TRET` / `SUPERVISE` / `DONE` / `ERRS` / `ABRT` states. Completion supervision nudges the model when it stops early; failed tool calls are sanitized. Transient API failures (`429` / `5xx`) retry with exponential backoff. Auth-expired codes (configurable via `AUTH_REFRESH_STATUS_CODES` in `config.py`; defaults to `[401, 502]`) trigger automatic API key re-read from config/env.
|
|
75
75
|
- **Context management** — CJK-aware token estimation, per-model context windows, automatic compaction at 70% usage.
|
|
76
76
|
- **Image & text attachments** — attach with `@path` (e.g. `@screenshot.png`, `@README.md`): images (PNG/JPEG/GIF/WebP, up to 20 MB) become multimodal `image_url` parts; text files are inlined. Web URLs supported for images.
|
|
77
|
-
- **Coding tools** — `Agent`, `TodoWrite`, `Glob`, `Grep`, `Read`, `
|
|
77
|
+
- **Coding tools** — `Agent`, `TodoWrite`, `Glob`, `Grep`, `Read`, `Edit` (incl. unified diffs), `Write`, `Bash`, `Skill`, `Question`, `PlanExit`. Read-only tool rounds (`Read`, `Glob`, `Grep`, `Skill`) dispatch concurrently; async tools (`Bash`, `Agent`) can also run concurrently. Results always delivered in emitted order.
|
|
78
78
|
- **Plan / Build modes** — plan mode is read-only except for the per-session plan file.
|
|
79
79
|
- **Persistent sessions** — auto-saved after every response to `~/.local/share/python-agent-harness/sessions/`, with LLM-generated titles and `/restore --latest` / `/sessions`.
|
|
80
80
|
- **Focused TUI** — Rich-based with pinned status bar, Todos panel, inline diff rendering, and a `prompt_toolkit` editor with history and completion. `Esc+Enter` submits, `Ctrl-D` quits, `Ctrl-C` cancels.
|
|
@@ -191,7 +191,6 @@ All LLM settings live in `~/.config/python-agent-harness/config.json`. Environme
|
|
|
191
191
|
- **`subagent_llm`** — LLM config for `Agent` tool. Unset values inherit from `llm`. Set `profile` to reuse a `models` entry. Precedence: profile > `subagent_llm` > `llm` > env vars.
|
|
192
192
|
- **`default_agent`** — agent name to use at session start (must exist in `prompts/agents/`). `null` = built-in default.
|
|
193
193
|
- **`paths.context_path` / `paths.skill_path`** — locations for context files and skills. Default: project-local `contexts/` and `skills/`.
|
|
194
|
-
- **`lsp.servers`** — per-extension LSP server overrides for the `LSP` tool. Keys are extensions (`.py`, `.cpp`); each has `command` (argv) and optional `language_id`. Layers on top of `DEFAULT_SERVERS` in `lsp/manager.py`.
|
|
195
194
|
- **`mcp.servers`** — MCP server config (requires `[mcp]` extra). Each supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, `enabled`.
|
|
196
195
|
- **`headless` / `serve`** — optional budget limits: `max_rounds` (LLM rounds) and `timeout` (wall-clock seconds). `--max-rounds` / `--timeout` override per call; `<= 0` or `null` disables. Unset = unlimited.
|
|
197
196
|
- **Precedence** — code defaults < config file < `OPENAI_*` env vars. Sub-agents also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
|
|
@@ -212,7 +211,7 @@ Most of [opencode](https://github.com/anomalyco/opencode)'s prompts and core beh
|
|
|
212
211
|
| `shell.txt` | `Bash` tool + `agent.md` Git/GitHub guidance |
|
|
213
212
|
| `plan-enter.txt` / `plan-exit.txt` | `PlanExit` tool |
|
|
214
213
|
| `initialize.txt` / `review.txt` / `explain` | `initialize.md` / `review.md` / `commands/explain.md` |
|
|
215
|
-
| compaction / summary / title | `compact.md` / `summary.md` / `title.md` |
|
|
214
|
+
| `compaction` / `summary` / `title` | `compact.md` / `summary.md` / `title.md` |
|
|
216
215
|
| `AGENTS.md` handling | `prompts.py` (`find_agents_md_files`, `load_context_files`) |
|
|
217
216
|
|
|
218
217
|
## Development
|
|
@@ -40,7 +40,7 @@ Edit `~/.config/python-agent-harness/config.json` and set your `base_url`, `api_
|
|
|
40
40
|
- **FSM-driven execution** — explicit `WAIT` / `TOOL` / `TRET` / `SUPERVISE` / `DONE` / `ERRS` / `ABRT` states. Completion supervision nudges the model when it stops early; failed tool calls are sanitized. Transient API failures (`429` / `5xx`) retry with exponential backoff. Auth-expired codes (configurable via `AUTH_REFRESH_STATUS_CODES` in `config.py`; defaults to `[401, 502]`) trigger automatic API key re-read from config/env.
|
|
41
41
|
- **Context management** — CJK-aware token estimation, per-model context windows, automatic compaction at 70% usage.
|
|
42
42
|
- **Image & text attachments** — attach with `@path` (e.g. `@screenshot.png`, `@README.md`): images (PNG/JPEG/GIF/WebP, up to 20 MB) become multimodal `image_url` parts; text files are inlined. Web URLs supported for images.
|
|
43
|
-
- **Coding tools** — `Agent`, `TodoWrite`, `Glob`, `Grep`, `Read`, `
|
|
43
|
+
- **Coding tools** — `Agent`, `TodoWrite`, `Glob`, `Grep`, `Read`, `Edit` (incl. unified diffs), `Write`, `Bash`, `Skill`, `Question`, `PlanExit`. Read-only tool rounds (`Read`, `Glob`, `Grep`, `Skill`) dispatch concurrently; async tools (`Bash`, `Agent`) can also run concurrently. Results always delivered in emitted order.
|
|
44
44
|
- **Plan / Build modes** — plan mode is read-only except for the per-session plan file.
|
|
45
45
|
- **Persistent sessions** — auto-saved after every response to `~/.local/share/python-agent-harness/sessions/`, with LLM-generated titles and `/restore --latest` / `/sessions`.
|
|
46
46
|
- **Focused TUI** — Rich-based with pinned status bar, Todos panel, inline diff rendering, and a `prompt_toolkit` editor with history and completion. `Esc+Enter` submits, `Ctrl-D` quits, `Ctrl-C` cancels.
|
|
@@ -157,7 +157,6 @@ All LLM settings live in `~/.config/python-agent-harness/config.json`. Environme
|
|
|
157
157
|
- **`subagent_llm`** — LLM config for `Agent` tool. Unset values inherit from `llm`. Set `profile` to reuse a `models` entry. Precedence: profile > `subagent_llm` > `llm` > env vars.
|
|
158
158
|
- **`default_agent`** — agent name to use at session start (must exist in `prompts/agents/`). `null` = built-in default.
|
|
159
159
|
- **`paths.context_path` / `paths.skill_path`** — locations for context files and skills. Default: project-local `contexts/` and `skills/`.
|
|
160
|
-
- **`lsp.servers`** — per-extension LSP server overrides for the `LSP` tool. Keys are extensions (`.py`, `.cpp`); each has `command` (argv) and optional `language_id`. Layers on top of `DEFAULT_SERVERS` in `lsp/manager.py`.
|
|
161
160
|
- **`mcp.servers`** — MCP server config (requires `[mcp]` extra). Each supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, `enabled`.
|
|
162
161
|
- **`headless` / `serve`** — optional budget limits: `max_rounds` (LLM rounds) and `timeout` (wall-clock seconds). `--max-rounds` / `--timeout` override per call; `<= 0` or `null` disables. Unset = unlimited.
|
|
163
162
|
- **Precedence** — code defaults < config file < `OPENAI_*` env vars. Sub-agents also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
|
|
@@ -178,7 +177,7 @@ Most of [opencode](https://github.com/anomalyco/opencode)'s prompts and core beh
|
|
|
178
177
|
| `shell.txt` | `Bash` tool + `agent.md` Git/GitHub guidance |
|
|
179
178
|
| `plan-enter.txt` / `plan-exit.txt` | `PlanExit` tool |
|
|
180
179
|
| `initialize.txt` / `review.txt` / `explain` | `initialize.md` / `review.md` / `commands/explain.md` |
|
|
181
|
-
| compaction / summary / title | `compact.md` / `summary.md` / `title.md` |
|
|
180
|
+
| `compaction` / `summary` / `title` | `compact.md` / `summary.md` / `title.md` |
|
|
182
181
|
| `AGENTS.md` handling | `prompts.py` (`find_agents_md_files`, `load_context_files`) |
|
|
183
182
|
|
|
184
183
|
## Development
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-agent-harness"
|
|
7
|
-
version = "1.5.6.
|
|
7
|
+
version = "1.5.6.5"
|
|
8
8
|
description = "A lightweight, hackable mini-OpenCode written in Python."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Huming Chen", email = "chenhuming@gmail.com"}
|
{python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/__init__.py
RENAMED
|
@@ -3,20 +3,17 @@
|
|
|
3
3
|
from .core.models import AgentMode, ImagePart, Message, TextPart, ToolCall, ToolSpec
|
|
4
4
|
from .entry.cli import main # noqa: F401 (public lib surface: main entry point)
|
|
5
5
|
from .llm.client import Client, LLMClient
|
|
6
|
-
from .lsp.config import LSPConfig, LSPServerConfig
|
|
7
6
|
from .mcp.config import MCPConfig, MCPServerConfig
|
|
8
7
|
from .mcp.manager import MCPManager
|
|
9
8
|
from .session.session import Session
|
|
10
9
|
|
|
11
|
-
__version__ = "1.5.6.
|
|
10
|
+
__version__ = "1.5.6.5"
|
|
12
11
|
|
|
13
12
|
__all__ = [
|
|
14
13
|
"Client",
|
|
15
14
|
"LLMClient",
|
|
16
15
|
"Session",
|
|
17
16
|
"AgentMode",
|
|
18
|
-
"LSPConfig",
|
|
19
|
-
"LSPServerConfig",
|
|
20
17
|
"MCPConfig",
|
|
21
18
|
"MCPManager",
|
|
22
19
|
"MCPServerConfig",
|
{python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/core/agent.py
RENAMED
|
@@ -471,8 +471,9 @@ class AgentLoop:
|
|
|
471
471
|
# after successful responses, so without this the
|
|
472
472
|
# interrupted turn would never reach the session
|
|
473
473
|
# file — the next turn's save would overwrite it
|
|
474
|
-
# without ever containing it.
|
|
475
|
-
|
|
474
|
+
# without ever containing it. flush bypasses the
|
|
475
|
+
# debounce so the partial turn is not lost.
|
|
476
|
+
session.flush_auto_save(salvaged)
|
|
476
477
|
# The title is generated on the first save, not
|
|
477
478
|
# only on clean completion — an interrupted session
|
|
478
479
|
# still gets a meaningful name (one-shot; no-op when
|
|
@@ -484,6 +485,9 @@ class AgentLoop:
|
|
|
484
485
|
# no-op when the title already exists or generation
|
|
485
486
|
# is in flight)
|
|
486
487
|
session.generate_session_title()
|
|
488
|
+
# The per-round auto_save is debounced; flush at run
|
|
489
|
+
# end so the final state always reaches the file.
|
|
490
|
+
session.flush_auto_save(salvaged)
|
|
487
491
|
return self.result
|
|
488
492
|
|
|
489
493
|
def _next_state(self) -> str:
|
|
@@ -552,6 +556,20 @@ class AgentLoop:
|
|
|
552
556
|
self._just_compacted = True
|
|
553
557
|
self.info["compacted"] = True
|
|
554
558
|
return
|
|
559
|
+
# Compaction failed (empty summary, no reducible
|
|
560
|
+
# prompts, or an exception -- see ContextManager.
|
|
561
|
+
# compact): the history is unchanged, so without a
|
|
562
|
+
# guard the next WAIT round sees the same
|
|
563
|
+
# above-trigger ratio and compacts again, re-billing
|
|
564
|
+
# the summary request every round for nothing.
|
|
565
|
+
# Remember the current ratio as the floor -- reusing
|
|
566
|
+
# the same mechanism as the "still above trigger
|
|
567
|
+
# after a successful compaction" case above -- so
|
|
568
|
+
# the guard below suppresses re-compaction until the
|
|
569
|
+
# context grows meaningfully past it, at which point
|
|
570
|
+
# a retry is warranted (the conversation really did
|
|
571
|
+
# grow) rather than a no-op loop.
|
|
572
|
+
self._compaction_floor = ratio
|
|
555
573
|
else:
|
|
556
574
|
self._compaction_floor = None
|
|
557
575
|
self._just_compacted = False
|
{python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/core/models.py
RENAMED
|
@@ -69,6 +69,15 @@ class ImagePart:
|
|
|
69
69
|
path: str | None = None
|
|
70
70
|
url: str | None = None
|
|
71
71
|
|
|
72
|
+
def __post_init__(self) -> None:
|
|
73
|
+
# to_api() runs on every round (token estimation + API payload),
|
|
74
|
+
# so the base64 data URL is encoded once, not per call.
|
|
75
|
+
self._data_url: str | None = None
|
|
76
|
+
if self.data is not None:
|
|
77
|
+
self._data_url = (
|
|
78
|
+
f"data:{self.media_type};base64,{base64.b64encode(self.data).decode('ascii')}"
|
|
79
|
+
)
|
|
80
|
+
|
|
72
81
|
@classmethod
|
|
73
82
|
def from_url(cls, url: str) -> ImagePart:
|
|
74
83
|
return cls(url=url)
|
|
@@ -76,13 +85,9 @@ class ImagePart:
|
|
|
76
85
|
def to_api(self) -> dict[str, Any]:
|
|
77
86
|
if self.url:
|
|
78
87
|
return {"type": "image_url", "image_url": {"url": self.url}}
|
|
79
|
-
if self.
|
|
88
|
+
if self._data_url is None:
|
|
80
89
|
raise ValueError("ImagePart requires either data or url")
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
"type": "image_url",
|
|
84
|
-
"image_url": {"url": f"data:{self.media_type};base64,{b64}"},
|
|
85
|
-
}
|
|
90
|
+
return {"type": "image_url", "image_url": {"url": self._data_url}}
|
|
86
91
|
|
|
87
92
|
|
|
88
93
|
# Type alias for the individual parts a Message.content list may contain.
|
|
@@ -155,6 +160,11 @@ class Message:
|
|
|
155
160
|
injected: bool = False # harness-injected (nudge/plan/build-switch), not user input
|
|
156
161
|
|
|
157
162
|
def to_api(self) -> dict[str, Any]:
|
|
163
|
+
# Not cached: the per-message dict is cheap to rebuild and
|
|
164
|
+
# caching it (returning the same object each call) would make a
|
|
165
|
+
# mutation by any caller corrupt later rounds and token
|
|
166
|
+
# estimation. The one expensive step — base64-encoding an image
|
|
167
|
+
# — is cached on ``ImagePart`` instead (see ImagePart.__post_init__).
|
|
158
168
|
d: dict[str, Any] = {"role": self.role}
|
|
159
169
|
if self.content is not None:
|
|
160
170
|
content = self._api_content()
|
|
@@ -45,7 +45,10 @@ def estimate_tokens(text: str) -> int:
|
|
|
45
45
|
"""Estimate tokens in TEXT: Latin ~4 chars/token, CJK ~2 chars/token."""
|
|
46
46
|
if not text:
|
|
47
47
|
return 0
|
|
48
|
-
|
|
48
|
+
# subn counts CJK chars in one C-speed pass without allocating the
|
|
49
|
+
# per-match list findall would build (this runs on the full payload
|
|
50
|
+
# every round).
|
|
51
|
+
cjk = _CJK_RE.subn("", text)[1]
|
|
49
52
|
latin = len(text) - cjk
|
|
50
53
|
return round(latin / 4.0 + cjk / 2.0)
|
|
51
54
|
|
{python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/entry/cli.py
RENAMED
|
@@ -45,11 +45,6 @@ def make_session(
|
|
|
45
45
|
settings = config.load_llm_config(config_path)
|
|
46
46
|
paths = config.load_paths_config(config_path)
|
|
47
47
|
mcp_config = config.load_mcp_config(config_path)
|
|
48
|
-
# Validate lsp.servers eagerly so a malformed override fails fast at
|
|
49
|
-
# session start (with a clear message) instead of poisoning the first
|
|
50
|
-
# LSP tool call mid-session. The LSP manager re-reads it lazily via
|
|
51
|
-
# config_path; this call only surfaces config errors early.
|
|
52
|
-
config.load_lsp_config(config_path)
|
|
53
48
|
model = model or settings["model"]
|
|
54
49
|
# resolve sub-agent overrides against the EFFECTIVE main settings
|
|
55
50
|
# (so a CLI/caller model override is inherited too when the
|
|
@@ -277,12 +272,20 @@ def cmd_serve(args: argparse.Namespace) -> int:
|
|
|
277
272
|
def cmd_config(args: argparse.Namespace) -> int:
|
|
278
273
|
path = config._config_path(args.path)
|
|
279
274
|
if args.init:
|
|
280
|
-
|
|
281
|
-
|
|
275
|
+
# 0o700 dir / 0o600 file: the config holds the API key, so it must
|
|
276
|
+
# not be group/world-readable (mode applies to a NEW leaf dir only;
|
|
277
|
+
# an existing directory keeps its permissions).
|
|
278
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
279
|
+
template = config.CONFIG_TEMPLATE.format(path=str(path).replace("\\", "\\\\"))
|
|
280
|
+
try:
|
|
281
|
+
# O_EXCL: create-only, atomically -- no exists()/write race, and
|
|
282
|
+
# a pre-planted symlink at PATH is refused instead of followed.
|
|
283
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
284
|
+
except FileExistsError:
|
|
282
285
|
print(f"config already exists: {path} (delete it first to overwrite)")
|
|
283
286
|
return 1
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
288
|
+
f.write(template)
|
|
286
289
|
print(f"wrote config template: {path}")
|
|
287
290
|
return 0
|
|
288
291
|
settings = config.load_llm_config(args.path)
|
|
@@ -345,14 +348,6 @@ def cmd_config(args: argparse.Namespace) -> int:
|
|
|
345
348
|
print(f" {pattern}: {size}")
|
|
346
349
|
else:
|
|
347
350
|
print("context_windows: (none configured — built-in table in config.py applies)")
|
|
348
|
-
# Show LSP server overrides (per-extension, layered over the built-ins)
|
|
349
|
-
lsp_config = config.load_lsp_config(args.path)
|
|
350
|
-
if lsp_config.servers:
|
|
351
|
-
print("lsp servers:")
|
|
352
|
-
for ext, server in sorted(lsp_config.servers.items()):
|
|
353
|
-
print(f" {ext}: language_id={server.language_id}, command={' '.join(server.command)}")
|
|
354
|
-
else:
|
|
355
|
-
print("lsp servers: (none configured — built-in DEFAULT_SERVERS in lsp/manager.py applies)")
|
|
356
351
|
# Show default agent
|
|
357
352
|
default_agent = config.load_default_agent(args.path)
|
|
358
353
|
print(f"default_agent: {default_agent or '(default: agent.md)'}")
|
{python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/entry/headless.py
RENAMED
|
@@ -59,8 +59,8 @@ def _as_error(value: Any) -> dict[str, Any]:
|
|
|
59
59
|
``HeadlessView.errors`` accumulates whatever the notifier passed:
|
|
60
60
|
the agent loop sends a structured ``{"code", "message"}`` for
|
|
61
61
|
classified failures (``budget``/``timeout``), other sites send a
|
|
62
|
-
plain string whose code is then derived from the text, and
|
|
63
|
-
|
|
62
|
+
plain string whose code is then derived from the text, and some
|
|
63
|
+
callers send a ``{"message": ...}`` dict. A nested
|
|
64
64
|
``{"error": {...}}`` shape is unwrapped; a dict carrying only a
|
|
65
65
|
``code`` falls back to that code for its message so the two never
|
|
66
66
|
disagree on emptiness.
|
|
@@ -106,8 +106,7 @@ def error_display_text(value: Any) -> str:
|
|
|
106
106
|
|
|
107
107
|
``notify("error", ...)`` carries either a plain message string or a
|
|
108
108
|
structured ``{"code", "message"}`` payload (the agent loop sends
|
|
109
|
-
the latter where the outcome is known
|
|
110
|
-
``{"message": ...}`` dict too). Views render this rather than the
|
|
109
|
+
the latter where the outcome is known). Views render this rather than the
|
|
111
110
|
raw value, so a structured error does not reach a human as a dict
|
|
112
111
|
repr.
|
|
113
112
|
|
|
@@ -138,6 +137,25 @@ def _cancelled_now(session: Any) -> bool:
|
|
|
138
137
|
return event is not None and event.is_set() is True
|
|
139
138
|
|
|
140
139
|
|
|
140
|
+
def usage_snapshot(session: Any) -> dict[str, Any] | None:
|
|
141
|
+
"""Cumulative input/output tokens and rounds of the current run.
|
|
142
|
+
|
|
143
|
+
Reads the CURRENT ``usage_totals`` (``Controller.submit`` swaps in a
|
|
144
|
+
fresh dict per run, so a reference captured before submit would
|
|
145
|
+
always snapshot zeros). None when the session keeps no totals.
|
|
146
|
+
"""
|
|
147
|
+
totals = getattr(session, "usage_totals", None)
|
|
148
|
+
if not isinstance(totals, dict):
|
|
149
|
+
return None
|
|
150
|
+
usage: dict[str, Any] = {"input": 0, "output": 0, "rounds": 0}
|
|
151
|
+
with totals.get("_lock", threading.Lock()):
|
|
152
|
+
for key in ("input", "output", "rounds"):
|
|
153
|
+
value = totals.get(key)
|
|
154
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
155
|
+
usage[key] = int(value)
|
|
156
|
+
return usage
|
|
157
|
+
|
|
158
|
+
|
|
141
159
|
def final_answer_text(session: Session) -> str:
|
|
142
160
|
"""The run's final assistant answer, TUI-filtered.
|
|
143
161
|
|
|
@@ -286,69 +304,25 @@ def restore_session(controller: Controller, spec: str, err: TextIO) -> bool:
|
|
|
286
304
|
file, and restores the saved agent/model. Returns False (with a
|
|
287
305
|
message on *err*) when no matching session exists.
|
|
288
306
|
"""
|
|
289
|
-
from
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
parse_saved_body,
|
|
293
|
-
title_from_filename,
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
if not spec or spec in ("--latest", "latest"):
|
|
297
|
-
path = SessionPersistence.latest_session()
|
|
298
|
-
elif os.path.isfile(os.path.expanduser(spec)):
|
|
299
|
-
path = os.path.expanduser(spec)
|
|
300
|
-
else:
|
|
301
|
-
path = find_session_by_title(spec)
|
|
307
|
+
from .restore import apply_saved_session, load_saved_session, resolve_session_path
|
|
308
|
+
|
|
309
|
+
path = resolve_session_path(spec)
|
|
302
310
|
if not path or not os.path.isfile(path):
|
|
303
311
|
err.write(f"no session found: {spec or 'latest'}\n")
|
|
304
312
|
return False
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
title = title_from_filename(path)
|
|
315
|
-
if title:
|
|
316
|
-
controller.store.title = title
|
|
317
|
-
agent = meta.get("python-agent-harness--agent")
|
|
318
|
-
if agent:
|
|
319
|
-
success, msg = controller.switch_agent(agent)
|
|
320
|
-
if not success:
|
|
321
|
-
controller.switch_agent("default")
|
|
313
|
+
try:
|
|
314
|
+
saved = load_saved_session(path)
|
|
315
|
+
except OSError as e:
|
|
316
|
+
err.write(f"cannot read {path}: {e}\n")
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
def report(level: str, msg: str) -> None:
|
|
320
|
+
# headless output is quiet on success: only problems reach stderr
|
|
321
|
+
if level == "warning":
|
|
322
322
|
err.write(f"warning: {msg}\n")
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
# The saved model is the session-start default: the
|
|
327
|
-
# "default" pseudo-profile restores exactly those settings.
|
|
328
|
-
controller.switch_model("default")
|
|
329
|
-
else:
|
|
330
|
-
success, _ = controller.switch_model(model)
|
|
331
|
-
if not success:
|
|
332
|
-
profile = next(
|
|
333
|
-
(
|
|
334
|
-
name
|
|
335
|
-
for name, p in controller.model_profiles.items()
|
|
336
|
-
if p.get("model") == model
|
|
337
|
-
),
|
|
338
|
-
None,
|
|
339
|
-
)
|
|
340
|
-
if profile:
|
|
341
|
-
success, _ = controller.switch_model(profile)
|
|
342
|
-
if not success:
|
|
343
|
-
err.write(
|
|
344
|
-
f"warning: model {model!r} has no matching profile, keeping current model\n"
|
|
345
|
-
)
|
|
346
|
-
# Replace conversation history: a new generation, so a stale worker
|
|
347
|
-
# from a previous run can't clobber the restored session.
|
|
348
|
-
controller.run_generation += 1
|
|
349
|
-
controller.last_messages = messages
|
|
350
|
-
controller.clear_todos()
|
|
351
|
-
err.write(f"restored: {os.path.basename(path)} ({len(messages)} messages)\n")
|
|
323
|
+
|
|
324
|
+
apply_saved_session(controller, saved, report)
|
|
325
|
+
err.write(f"restored: {os.path.basename(path)} ({len(saved.messages)} messages)\n")
|
|
352
326
|
return True
|
|
353
327
|
|
|
354
328
|
|
|
@@ -680,20 +654,12 @@ def run_headless_jsonl(
|
|
|
680
654
|
"""The terminal result line: snapshots usage and model so every
|
|
681
655
|
result line — including early failures — carries the same
|
|
682
656
|
fields for the driving process."""
|
|
683
|
-
usage: dict[str, Any] | None = None
|
|
684
|
-
# Read the CURRENT usage_totals, not a reference captured before
|
|
685
|
-
# submit(): Controller.submit swaps in a fresh dict per run, so
|
|
686
|
-
# a pre-submit reference would always snapshot zeros.
|
|
687
|
-
totals = getattr(session, "usage_totals", None)
|
|
688
|
-
if isinstance(totals, dict):
|
|
689
|
-
usage = {"input": 0, "output": 0, "rounds": 0}
|
|
690
|
-
with totals.get("_lock", threading.Lock()):
|
|
691
|
-
for key in ("input", "output", "rounds"):
|
|
692
|
-
value = totals.get(key)
|
|
693
|
-
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
694
|
-
usage[key] = int(value)
|
|
695
657
|
view.emit_result(
|
|
696
|
-
answer,
|
|
658
|
+
answer,
|
|
659
|
+
errors=errors,
|
|
660
|
+
usage=usage_snapshot(session),
|
|
661
|
+
model=session.model,
|
|
662
|
+
cancelled=cancelled,
|
|
697
663
|
)
|
|
698
664
|
|
|
699
665
|
if restore is not None and not restore_session(controller, restore, view.err):
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Shared session-restore logic for the TUI's /restore and headless --restore.
|
|
2
|
+
|
|
3
|
+
Both front ends resolve a saved session the same way (path, title
|
|
4
|
+
substring, or latest), parse it the same way, and apply it to a
|
|
5
|
+
``Controller`` the same way: repoint the session store, restore the
|
|
6
|
+
saved agent and model, and replace the conversation history. Only the
|
|
7
|
+
reporting differs, so callers pass a ``report(level, message)``
|
|
8
|
+
callback ("ok" for confirmations, "warning" for problems).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from ..core.models import Message
|
|
19
|
+
from ..io import persistence
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .controller import Controller
|
|
23
|
+
|
|
24
|
+
AGENT_KEY = "python-agent-harness--agent"
|
|
25
|
+
MODEL_KEY = "python-agent-harness--model"
|
|
26
|
+
PROJECT_KEY = "python-agent-harness--project-dir"
|
|
27
|
+
ROUND_TIMES_KEY = "python-agent-harness--round-times"
|
|
28
|
+
|
|
29
|
+
Report = Callable[[str, str], None]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class SavedSession:
|
|
34
|
+
"""A parsed session file: its path, metadata and conversation."""
|
|
35
|
+
|
|
36
|
+
path: str
|
|
37
|
+
meta: dict[str, str] = field(default_factory=dict)
|
|
38
|
+
messages: list[Message] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def round_times(self) -> list[float]:
|
|
42
|
+
"""Persisted round timestamps; [] when absent or malformed."""
|
|
43
|
+
raw = self.meta.get(ROUND_TIMES_KEY)
|
|
44
|
+
if not raw:
|
|
45
|
+
return []
|
|
46
|
+
try:
|
|
47
|
+
return [float(x) for x in raw.split()]
|
|
48
|
+
except ValueError:
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_session_path(spec: str) -> str | None:
|
|
53
|
+
"""Map SPEC (empty/"latest"/"--latest", a file path, or a title
|
|
54
|
+
substring) to a session file path, or None when nothing matches.
|
|
55
|
+
|
|
56
|
+
The result is not guaranteed to exist: callers check ``isfile``.
|
|
57
|
+
"""
|
|
58
|
+
if not spec or spec in ("--latest", "latest"):
|
|
59
|
+
return persistence.SessionPersistence.latest_session()
|
|
60
|
+
expanded = os.path.expanduser(spec)
|
|
61
|
+
if os.path.isfile(expanded):
|
|
62
|
+
return expanded
|
|
63
|
+
return persistence.find_session_by_title(spec)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_saved_session(path: str) -> SavedSession:
|
|
67
|
+
"""Read and parse the session file at PATH. Raises OSError.
|
|
68
|
+
|
|
69
|
+
surrogateescape mirrors ``SessionPersistence.save``: the saved body
|
|
70
|
+
may carry lone surrogates (non-UTF-8 file content), which a strict
|
|
71
|
+
decode would reject.
|
|
72
|
+
"""
|
|
73
|
+
with open(path, encoding="utf-8", errors="surrogateescape") as fh:
|
|
74
|
+
text = fh.read()
|
|
75
|
+
store = persistence.SessionPersistence
|
|
76
|
+
meta = store.parse_metadata(text)
|
|
77
|
+
messages = persistence.parse_saved_body(store.strip_metadata(text))
|
|
78
|
+
return SavedSession(path=path, meta=meta, messages=messages)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def restore_agent(controller: Controller, agent: str | None, report: Report) -> None:
|
|
82
|
+
"""Switch to the saved AGENT; fall back to the default agent (so no
|
|
83
|
+
stale tool exclusions linger) when it no longer exists."""
|
|
84
|
+
if not agent:
|
|
85
|
+
return
|
|
86
|
+
try:
|
|
87
|
+
success, msg = controller.switch_agent(agent)
|
|
88
|
+
if success:
|
|
89
|
+
report("ok", msg)
|
|
90
|
+
else:
|
|
91
|
+
controller.switch_agent("default")
|
|
92
|
+
report("warning", msg)
|
|
93
|
+
except Exception as e: # noqa: BLE001 - a bad agent must not abort the restore
|
|
94
|
+
report("warning", f"could not restore agent: {e}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def restore_model(controller: Controller, model: str | None, report: Report) -> None:
|
|
98
|
+
"""Switch back to the saved raw MODEL name through a profile.
|
|
99
|
+
|
|
100
|
+
Profile switching also fixes base_url/api_key/temperature, which a
|
|
101
|
+
bare model-name change would leave pointing at the wrong endpoint.
|
|
102
|
+
Order: the "default" pseudo-profile when MODEL is the session-start
|
|
103
|
+
model, else MODEL as a profile name, else a profile whose model
|
|
104
|
+
matches. Without a match the current model stays active.
|
|
105
|
+
"""
|
|
106
|
+
if not model or model == controller.model:
|
|
107
|
+
return
|
|
108
|
+
try:
|
|
109
|
+
if model == controller.llm_settings.get("model"):
|
|
110
|
+
_, msg = controller.switch_model("default")
|
|
111
|
+
report("ok", msg)
|
|
112
|
+
return
|
|
113
|
+
success, msg = controller.switch_model(model)
|
|
114
|
+
if not success:
|
|
115
|
+
profile = next(
|
|
116
|
+
(name for name, p in controller.model_profiles.items() if p.get("model") == model),
|
|
117
|
+
None,
|
|
118
|
+
)
|
|
119
|
+
if profile:
|
|
120
|
+
success, msg = controller.switch_model(profile)
|
|
121
|
+
if success:
|
|
122
|
+
report("ok", msg)
|
|
123
|
+
else:
|
|
124
|
+
report("warning", f"model {model!r} has no matching profile, keeping current model")
|
|
125
|
+
except Exception as e: # noqa: BLE001 - a bad model must not abort the restore
|
|
126
|
+
report("warning", f"could not restore model: {e}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def apply_saved_session(controller: Controller, saved: SavedSession, report: Report) -> None:
|
|
130
|
+
"""Make SAVED the controller's current session.
|
|
131
|
+
|
|
132
|
+
The conversation history is replaced in BOTH places the runtime
|
|
133
|
+
reads it: ``conversation_history`` (what the next run sends) and
|
|
134
|
+
``last_messages`` (what the views render). ``run_generation`` is
|
|
135
|
+
bumped so a worker still winding down from a cancelled run cannot
|
|
136
|
+
clobber the restored history with its salvaged one.
|
|
137
|
+
"""
|
|
138
|
+
store = controller.store
|
|
139
|
+
store.round_times = saved.round_times
|
|
140
|
+
store.file_path = saved.path
|
|
141
|
+
title = persistence.title_from_filename(saved.path)
|
|
142
|
+
if title:
|
|
143
|
+
store.title = title
|
|
144
|
+
restore_agent(controller, saved.meta.get(AGENT_KEY), report)
|
|
145
|
+
restore_model(controller, saved.meta.get(MODEL_KEY), report)
|
|
146
|
+
controller.run_generation += 1
|
|
147
|
+
controller.conversation_history = list(saved.messages)
|
|
148
|
+
controller.last_messages = list(saved.messages)
|
|
149
|
+
controller.clear_todos()
|
{python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/entry/server.py
RENAMED
|
@@ -130,7 +130,14 @@ from typing import Any, TextIO
|
|
|
130
130
|
from ..core.models import api_safe
|
|
131
131
|
from ..session.session import Session
|
|
132
132
|
from .controller import Controller
|
|
133
|
-
from .headless import
|
|
133
|
+
from .headless import (
|
|
134
|
+
PROTOCOL_VERSION,
|
|
135
|
+
JsonlView,
|
|
136
|
+
_cancelled_now,
|
|
137
|
+
_restorable_handler,
|
|
138
|
+
final_answer_text,
|
|
139
|
+
usage_snapshot,
|
|
140
|
+
)
|
|
134
141
|
|
|
135
142
|
DEFAULT_ANSWER_TIMEOUT = 0.0 # wait forever for a host answer
|
|
136
143
|
|
|
@@ -581,8 +588,7 @@ class AgentServer:
|
|
|
581
588
|
# -- run execution ------------------------------------------------------------
|
|
582
589
|
|
|
583
590
|
def _cancelled_now(self) -> bool:
|
|
584
|
-
|
|
585
|
-
return event is not None and event.is_set() is True
|
|
591
|
+
return _cancelled_now(self.session)
|
|
586
592
|
|
|
587
593
|
def _final_answer(self) -> str:
|
|
588
594
|
"""The run's final assistant answer, TUI-filtered (headless shape).
|
|
@@ -595,16 +601,7 @@ class AgentServer:
|
|
|
595
601
|
|
|
596
602
|
def _usage_snapshot(self) -> dict[str, Any] | None:
|
|
597
603
|
"""Cumulative input/output tokens and rounds of the current run."""
|
|
598
|
-
|
|
599
|
-
if not isinstance(totals, dict):
|
|
600
|
-
return None
|
|
601
|
-
usage: dict[str, Any] = {"input": 0, "output": 0, "rounds": 0}
|
|
602
|
-
with totals.get("_lock", threading.Lock()):
|
|
603
|
-
for key in ("input", "output", "rounds"):
|
|
604
|
-
value = totals.get(key)
|
|
605
|
-
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
606
|
-
usage[key] = int(value)
|
|
607
|
-
return usage
|
|
604
|
+
return usage_snapshot(self.session)
|
|
608
605
|
|
|
609
606
|
def _execute_run(self, prompt: str, run_id: str) -> None:
|
|
610
607
|
"""Run one prompt to completion, streaming lines as they happen.
|
{python_agent_harness-1.5.6.3 → python_agent_harness-1.5.6.5}/python_agent_harness/prompts/agent.md
RENAMED
|
@@ -56,6 +56,7 @@ The user will primarily request you perform software engineering tasks. This inc
|
|
|
56
56
|
- Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
|
|
57
57
|
- Implement the solution using all tools available to you
|
|
58
58
|
- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
|
|
59
|
+
- Test edge cases to ensure your solution is robust.
|
|
59
60
|
- VERY IMPORTANT:
|
|
60
61
|
- When you have completed a task, you MUST run the lint and typecheck commands (e.g. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time.
|
|
61
62
|
- NEVER commit changes unless the user explicitly asks you to.
|