python-agent-harness 1.5.6.0__tar.gz → 1.5.6.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.6.0/python_agent_harness.egg-info → python_agent_harness-1.5.6.1}/PKG-INFO +93 -17
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/README.md +92 -16
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/pyproject.toml +1 -1
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/core/agent.py +47 -5
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/core/models.py +33 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/cli.py +58 -11
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/controller.py +15 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/headless.py +203 -22
- python_agent_harness-1.5.6.1/python_agent_harness/entry/server.py +1003 -0
- python_agent_harness-1.5.6.1/python_agent_harness/entry/view.py +110 -0
- python_agent_harness-1.5.6.1/python_agent_harness/io/text_filter.py +262 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/session/config.py +48 -11
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/session/session.py +67 -8
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/read.py +0 -1
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/core.py +16 -2
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/render.py +2 -26
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1/python_agent_harness.egg-info}/PKG-INFO +93 -17
- python_agent_harness-1.5.6.0/python_agent_harness/entry/server.py +0 -534
- python_agent_harness-1.5.6.0/python_agent_harness/entry/view.py +0 -51
- python_agent_harness-1.5.6.0/python_agent_harness/io/text_filter.py +0 -84
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/LICENSE +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/core/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/core/context_manager.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/core/planmode.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/core/subagent.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/core/token_estimator.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/core/tool_runner.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/io/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/io/attachments.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/io/diffrender.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/io/persistence.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/llm/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/llm/client.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/client.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/config.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/lsp/manager.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/agents/reviewer.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/core.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/session/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/session/commands.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/diffapply.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/edit.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/glob.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/glob_mac.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/grep.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/grep_mac.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/insert.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/lsp.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/commands.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/SOURCES.txt +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/setup.cfg +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/tests/test_invariants.py +0 -0
- {python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/tests/test_scenarios.py +0 -0
{python_agent_harness-1.5.6.0/python_agent_harness.egg-info → python_agent_harness-1.5.6.1}/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.1
|
|
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
|
|
@@ -222,6 +222,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
222
222
|
- **`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.
|
|
223
223
|
- **`lsp.servers`** — optional per-extension LSP server overrides for the `LSP` code-intelligence tool. Keys are file extensions (e.g. `.py`, `.cpp`); each value has a `command` (the server argv) and an optional `language_id` (defaults to the extension without its dot). These layer on top of the built-in `DEFAULT_SERVERS` table in `lsp/manager.py`; an entry for an existing extension replaces its default. The server binary must be on `PATH`.
|
|
224
224
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
225
|
+
- **`headless`** — optional server-side limits for unattended `headless` runs: `max_rounds` caps LLM rounds and `timeout` caps wall-clock seconds. `--max-rounds` / `--timeout` override these per call; values `<= 0` or `null` disable. Unset = unlimited (interactive behavior).
|
|
226
|
+
- **`serve`** — the same two keys for the resident `serve` server, applied to **each** run rather than the process: the server is resident, so every `submit` gets a fresh budget. Enforced in-process so a driving host cannot raise its own ceiling — the ceiling belongs to whoever starts the sandbox. A tripped budget ends the run with a normal `result` line carrying `errors[].code` of `budget` or `timeout` plus the token usage consumed. `--max-rounds` / `--timeout` override these; values `<= 0` or `null` disable. Unset = unlimited.
|
|
225
227
|
- **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
|
|
226
228
|
- **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
|
|
227
229
|
- **LLM logging** — request and response bodies are logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change the directory. The log path is printed at startup.
|
|
@@ -271,9 +273,16 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
271
273
|
one `{"type": ...}` object per line: `start` (echoes the prompt and
|
|
272
274
|
submit warnings), `delta` (streamed text chunks), `notify` (tool and
|
|
273
275
|
status events, with `kind`/`data`), `log`, and a final `result` (the
|
|
274
|
-
filtered answer plus any `errors`).
|
|
275
|
-
|
|
276
|
-
|
|
276
|
+
filtered answer plus any `errors`). Every line carries `protocol`
|
|
277
|
+
(the wire schema version — reject an unknown version before
|
|
278
|
+
consuming events) and `seq` (a per-stream monotonic counter for
|
|
279
|
+
drop/reorder detection). The `result` line reports `errors` as a
|
|
280
|
+
structured list of `{"code", "message"}` objects (codes: `budget`,
|
|
281
|
+
`timeout`, `nothing`, `restore`, `no_answer`, `unknown`, ...) with an
|
|
282
|
+
`error_messages` flat-string mirror for simple drivers. Diagnostics
|
|
283
|
+
(restore/model notes, a plain-text echo of error events) still go to
|
|
284
|
+
stderr, so one pipe carries the structured stream. Exit codes are
|
|
285
|
+
unchanged.
|
|
277
286
|
|
|
278
287
|
```sh
|
|
279
288
|
python-agent-harness headless "fix it" --json | jq -c 'select(.type=="result")'
|
|
@@ -286,6 +295,7 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
286
295
|
|
|
287
296
|
```sh
|
|
288
297
|
python-agent-harness serve [--project DIR] [--answer-timeout SECONDS]
|
|
298
|
+
[--max-rounds N] [--timeout SECONDS]
|
|
289
299
|
```
|
|
290
300
|
|
|
291
301
|
A persistent, bidirectional runtime boundary for hosting applications
|
|
@@ -306,23 +316,87 @@ Protocol (one JSON object per line):
|
|
|
306
316
|
|
|
307
317
|
```
|
|
308
318
|
host → agent: {"op": "submit", "prompt": ..., "run_id": ...}
|
|
309
|
-
{"op": "answer", "run_id": ..., "answers": [...]}
|
|
319
|
+
{"op": "answer", "run_id": ..., "answers": [...], "ask_id": ...}
|
|
310
320
|
{"op": "cancel", "run_id": ...}
|
|
321
|
+
{"op": "hello", "protocol_versions": [...]}
|
|
311
322
|
{"op": "ping"} | {"op": "shutdown"}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
323
|
+
... any op may carry "op_id", echoed on what it causes
|
|
324
|
+
agent → host: {"protocol": V, "type": "ready", "pid": ...,
|
|
325
|
+
"protocol_version": V, "capabilities": [...]} first line
|
|
326
|
+
{"protocol": V, "type": "hello", "protocol_version": V,
|
|
327
|
+
"capabilities": [...], "op_id": ...} negotiation reply
|
|
328
|
+
{"protocol": V, "seq": N, "type": "start"|"delta"|"notify"|"log",
|
|
329
|
+
"run_id": ...}
|
|
330
|
+
{"protocol": V, "seq": N, "type": "result", "run_id": ...,
|
|
331
|
+
"answer": ..., "errors": [{"code", "message"}],
|
|
332
|
+
"error_messages": [...], "usage": {...}, "cancelled": bool}
|
|
333
|
+
{"protocol": V, "type": "error",
|
|
334
|
+
"error": {"code": "protocol", "message": ...},
|
|
335
|
+
"message": ...} protocol failures
|
|
317
336
|
```
|
|
318
337
|
|
|
338
|
+
Version negotiation runs both ways. `ready` announces what the build
|
|
339
|
+
speaks — a single integer a host can only accept or reject wholesale.
|
|
340
|
+
`hello` is the other direction: the host states the versions *it* can
|
|
341
|
+
parse and the server confirms a shared one or refuses with a
|
|
342
|
+
`protocol` error, so a mismatch is settled once, before any run,
|
|
343
|
+
rather than surfacing as misparsed events. With one line shape today
|
|
344
|
+
the only outcomes are "agreed" or "no overlap", but it is the hook a
|
|
345
|
+
future version needs in order to downgrade instead of breaking an
|
|
346
|
+
older host. Both halves are advertised as the `hello` capability.
|
|
347
|
+
|
|
348
|
+
There is no generic `ack`: each op is acknowledged by its own effect
|
|
349
|
+
on the stream. Failures are the exception, because an effect that
|
|
350
|
+
never happened cannot be correlated — so an op may carry an `op_id`,
|
|
351
|
+
echoed on the `error` (or `pong`) it causes. A host that pipelined an
|
|
352
|
+
`answer` and a `cancel` can then tell which one was refused instead of
|
|
353
|
+
seeing two indistinguishable error lines. Advertised as the `op_id`
|
|
354
|
+
capability; omitting the field keeps the older behaviour.
|
|
355
|
+
|
|
319
356
|
A mid-run question arrives as a `notify` with `kind: "ask"` (data has
|
|
320
|
-
`kind: "ask"|"confirm"`); reply with `answer
|
|
321
|
-
`
|
|
322
|
-
|
|
323
|
-
for
|
|
324
|
-
|
|
325
|
-
|
|
357
|
+
`kind: "ask"|"confirm"` and an `ask_id`); reply with `answer`, echoing
|
|
358
|
+
the `ask_id` so the reply is matched to its question. A mismatched id
|
|
359
|
+
is refused rather than applied to whatever is pending — otherwise a
|
|
360
|
+
reply sent for a question that has since timed out would resolve the
|
|
361
|
+
*next* one. Omitting `ask_id` keeps the older behaviour, except once
|
|
362
|
+
some ask in the run has timed out, when an uncorrelated answer is
|
|
363
|
+
ambiguous and is refused.
|
|
364
|
+
|
|
365
|
+
One run at a time; a `submit` while one is active is rejected with an
|
|
366
|
+
`error` line. `--answer-timeout SECONDS` bounds how long a pending
|
|
367
|
+
question waits for the host's answer (default: forever). The result
|
|
368
|
+
line's shape is identical to `headless --json`'s, so a driver can
|
|
369
|
+
speak both protocols with one parser.
|
|
370
|
+
|
|
371
|
+
`ready` carries a `capabilities` list naming the features the build
|
|
372
|
+
supports, so a host can adapt instead of inferring a feature's absence
|
|
373
|
+
from events that never arrive. A `notify` of kind `usage` reports the
|
|
374
|
+
run's running `{input, output, rounds}` after each round, so a host can
|
|
375
|
+
meter mid-run and `cancel` a run that outruns its budget rather than
|
|
376
|
+
learning the cost only from the terminal `result`. Classified
|
|
377
|
+
failures carry their code from the raise site (`errors[].code` of
|
|
378
|
+
`budget` or `timeout`), so a driver never has to match words in a
|
|
379
|
+
human-readable message.
|
|
380
|
+
|
|
381
|
+
`--max-rounds N` and `--timeout SECONDS` (or the config file's `serve`
|
|
382
|
+
section) bound **each** run rather than the process — the resident
|
|
383
|
+
server grants every `submit` a fresh budget. Both are off by default.
|
|
384
|
+
They are deliberately not fields on the `submit` op: `serve` sandboxes
|
|
385
|
+
untrusted agent code on a host's behalf, so the ceiling belongs to
|
|
386
|
+
whoever starts the sandbox, not to the caller. A tripped budget
|
|
387
|
+
unwinds the agent loop through its normal path, so the run still ends
|
|
388
|
+
with a `result` line carrying `errors[].code` of `budget` or `timeout`
|
|
389
|
+
plus the token usage consumed — a host bills and reports it like any
|
|
390
|
+
other outcome.
|
|
391
|
+
|
|
392
|
+
`timeout` is **cooperative** — checked between rounds, not by a
|
|
393
|
+
watchdog — so it bounds work at those checkpoints but does not
|
|
394
|
+
forcibly interrupt a blocking operation mid-round (a hung tool, a slow
|
|
395
|
+
network read, or a run parked on a mid-run question awaiting a human
|
|
396
|
+
answer). Use `--answer-timeout` to cap how long an ask waits, and rely
|
|
397
|
+
on the hosting process's own exec watchdog (which sends `cancel` then
|
|
398
|
+
kills) as the hard wall-clock stop for anything the cooperative budget
|
|
399
|
+
cannot reach.
|
|
326
400
|
|
|
327
401
|
### Slash commands
|
|
328
402
|
|
|
@@ -340,6 +414,7 @@ protocols with one parser.
|
|
|
340
414
|
| `/clear` | Start a fresh conversation |
|
|
341
415
|
| `/model [name]` | Switch LLM profiles; `default` restores the session's original model |
|
|
342
416
|
| `/agent [name]` | Switch agent system prompt; `default` restores the built-in `agent.md` |
|
|
417
|
+
| `/help` | List the available commands |
|
|
343
418
|
| `/exit` | Quit |
|
|
344
419
|
|
|
345
420
|
Custom commands from `prompts/commands/*.md` are registered as slash commands as well (TUI only).
|
|
@@ -384,12 +459,13 @@ Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux a
|
|
|
384
459
|
make test # unit tests
|
|
385
460
|
venv/bin/pip install -e ".[dev]" # development tools
|
|
386
461
|
venv/bin/ruff check . # lint
|
|
462
|
+
venv/bin/ruff format --check . # formatting (CI blocks on this too)
|
|
387
463
|
venv/bin/pyright # type checking
|
|
388
464
|
venv/bin/python -m build # build sdist + wheel
|
|
389
465
|
venv/bin/pip-audit # dependency audit
|
|
390
466
|
```
|
|
391
467
|
|
|
392
|
-
CI blocks on Ruff and Pyright failures.
|
|
468
|
+
CI blocks on Ruff (lint **and** `format --check`) and Pyright failures.
|
|
393
469
|
|
|
394
470
|
## Design philosophy
|
|
395
471
|
|
|
@@ -188,6 +188,8 @@ All LLM settings live in a single JSON configuration file. Environment variables
|
|
|
188
188
|
- **`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.
|
|
189
189
|
- **`lsp.servers`** — optional per-extension LSP server overrides for the `LSP` code-intelligence tool. Keys are file extensions (e.g. `.py`, `.cpp`); each value has a `command` (the server argv) and an optional `language_id` (defaults to the extension without its dot). These layer on top of the built-in `DEFAULT_SERVERS` table in `lsp/manager.py`; an entry for an existing extension replaces its default. The server binary must be on `PATH`.
|
|
190
190
|
- **`mcp.servers`** — MCP server configuration. Requires the `[mcp]` extra. Each server supports `transport`, `command`, `args`, `env`, `url`, `headers`, `parallel`, `timeout`, and `enabled`.
|
|
191
|
+
- **`headless`** — optional server-side limits for unattended `headless` runs: `max_rounds` caps LLM rounds and `timeout` caps wall-clock seconds. `--max-rounds` / `--timeout` override these per call; values `<= 0` or `null` disable. Unset = unlimited (interactive behavior).
|
|
192
|
+
- **`serve`** — the same two keys for the resident `serve` server, applied to **each** run rather than the process: the server is resident, so every `submit` gets a fresh budget. Enforced in-process so a driving host cannot raise its own ceiling — the ceiling belongs to whoever starts the sandbox. A tripped budget ends the run with a normal `result` line carrying `errors[].code` of `budget` or `timeout` plus the token usage consumed. `--max-rounds` / `--timeout` override these; values `<= 0` or `null` disable. Unset = unlimited.
|
|
191
193
|
- **Configuration precedence** — code defaults < config file < `OPENAI_*` environment variables. Sub-agent settings also support `OPENAI_SUBAGENT_*` (`_BASE_URL`, `_API_KEY`, `_MODEL`).
|
|
192
194
|
- **Custom config** — use `--config PATH` or `PYTHON_AGENT_HARNESS_CONFIG`.
|
|
193
195
|
- **LLM logging** — request and response bodies are logged as JSON to `/tmp/python-agent-harness-<date>-<id>.json`. Set `LLM_LOG_DIR` to change the directory. The log path is printed at startup.
|
|
@@ -237,9 +239,16 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
237
239
|
one `{"type": ...}` object per line: `start` (echoes the prompt and
|
|
238
240
|
submit warnings), `delta` (streamed text chunks), `notify` (tool and
|
|
239
241
|
status events, with `kind`/`data`), `log`, and a final `result` (the
|
|
240
|
-
filtered answer plus any `errors`).
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
filtered answer plus any `errors`). Every line carries `protocol`
|
|
243
|
+
(the wire schema version — reject an unknown version before
|
|
244
|
+
consuming events) and `seq` (a per-stream monotonic counter for
|
|
245
|
+
drop/reorder detection). The `result` line reports `errors` as a
|
|
246
|
+
structured list of `{"code", "message"}` objects (codes: `budget`,
|
|
247
|
+
`timeout`, `nothing`, `restore`, `no_answer`, `unknown`, ...) with an
|
|
248
|
+
`error_messages` flat-string mirror for simple drivers. Diagnostics
|
|
249
|
+
(restore/model notes, a plain-text echo of error events) still go to
|
|
250
|
+
stderr, so one pipe carries the structured stream. Exit codes are
|
|
251
|
+
unchanged.
|
|
243
252
|
|
|
244
253
|
```sh
|
|
245
254
|
python-agent-harness headless "fix it" --json | jq -c 'select(.type=="result")'
|
|
@@ -252,6 +261,7 @@ Interactive prompts are auto-answered (`confirm` → yes, `ask` →
|
|
|
252
261
|
|
|
253
262
|
```sh
|
|
254
263
|
python-agent-harness serve [--project DIR] [--answer-timeout SECONDS]
|
|
264
|
+
[--max-rounds N] [--timeout SECONDS]
|
|
255
265
|
```
|
|
256
266
|
|
|
257
267
|
A persistent, bidirectional runtime boundary for hosting applications
|
|
@@ -272,23 +282,87 @@ Protocol (one JSON object per line):
|
|
|
272
282
|
|
|
273
283
|
```
|
|
274
284
|
host → agent: {"op": "submit", "prompt": ..., "run_id": ...}
|
|
275
|
-
{"op": "answer", "run_id": ..., "answers": [...]}
|
|
285
|
+
{"op": "answer", "run_id": ..., "answers": [...], "ask_id": ...}
|
|
276
286
|
{"op": "cancel", "run_id": ...}
|
|
287
|
+
{"op": "hello", "protocol_versions": [...]}
|
|
277
288
|
{"op": "ping"} | {"op": "shutdown"}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
... any op may carry "op_id", echoed on what it causes
|
|
290
|
+
agent → host: {"protocol": V, "type": "ready", "pid": ...,
|
|
291
|
+
"protocol_version": V, "capabilities": [...]} first line
|
|
292
|
+
{"protocol": V, "type": "hello", "protocol_version": V,
|
|
293
|
+
"capabilities": [...], "op_id": ...} negotiation reply
|
|
294
|
+
{"protocol": V, "seq": N, "type": "start"|"delta"|"notify"|"log",
|
|
295
|
+
"run_id": ...}
|
|
296
|
+
{"protocol": V, "seq": N, "type": "result", "run_id": ...,
|
|
297
|
+
"answer": ..., "errors": [{"code", "message"}],
|
|
298
|
+
"error_messages": [...], "usage": {...}, "cancelled": bool}
|
|
299
|
+
{"protocol": V, "type": "error",
|
|
300
|
+
"error": {"code": "protocol", "message": ...},
|
|
301
|
+
"message": ...} protocol failures
|
|
283
302
|
```
|
|
284
303
|
|
|
304
|
+
Version negotiation runs both ways. `ready` announces what the build
|
|
305
|
+
speaks — a single integer a host can only accept or reject wholesale.
|
|
306
|
+
`hello` is the other direction: the host states the versions *it* can
|
|
307
|
+
parse and the server confirms a shared one or refuses with a
|
|
308
|
+
`protocol` error, so a mismatch is settled once, before any run,
|
|
309
|
+
rather than surfacing as misparsed events. With one line shape today
|
|
310
|
+
the only outcomes are "agreed" or "no overlap", but it is the hook a
|
|
311
|
+
future version needs in order to downgrade instead of breaking an
|
|
312
|
+
older host. Both halves are advertised as the `hello` capability.
|
|
313
|
+
|
|
314
|
+
There is no generic `ack`: each op is acknowledged by its own effect
|
|
315
|
+
on the stream. Failures are the exception, because an effect that
|
|
316
|
+
never happened cannot be correlated — so an op may carry an `op_id`,
|
|
317
|
+
echoed on the `error` (or `pong`) it causes. A host that pipelined an
|
|
318
|
+
`answer` and a `cancel` can then tell which one was refused instead of
|
|
319
|
+
seeing two indistinguishable error lines. Advertised as the `op_id`
|
|
320
|
+
capability; omitting the field keeps the older behaviour.
|
|
321
|
+
|
|
285
322
|
A mid-run question arrives as a `notify` with `kind: "ask"` (data has
|
|
286
|
-
`kind: "ask"|"confirm"`); reply with `answer
|
|
287
|
-
`
|
|
288
|
-
|
|
289
|
-
for
|
|
290
|
-
|
|
291
|
-
|
|
323
|
+
`kind: "ask"|"confirm"` and an `ask_id`); reply with `answer`, echoing
|
|
324
|
+
the `ask_id` so the reply is matched to its question. A mismatched id
|
|
325
|
+
is refused rather than applied to whatever is pending — otherwise a
|
|
326
|
+
reply sent for a question that has since timed out would resolve the
|
|
327
|
+
*next* one. Omitting `ask_id` keeps the older behaviour, except once
|
|
328
|
+
some ask in the run has timed out, when an uncorrelated answer is
|
|
329
|
+
ambiguous and is refused.
|
|
330
|
+
|
|
331
|
+
One run at a time; a `submit` while one is active is rejected with an
|
|
332
|
+
`error` line. `--answer-timeout SECONDS` bounds how long a pending
|
|
333
|
+
question waits for the host's answer (default: forever). The result
|
|
334
|
+
line's shape is identical to `headless --json`'s, so a driver can
|
|
335
|
+
speak both protocols with one parser.
|
|
336
|
+
|
|
337
|
+
`ready` carries a `capabilities` list naming the features the build
|
|
338
|
+
supports, so a host can adapt instead of inferring a feature's absence
|
|
339
|
+
from events that never arrive. A `notify` of kind `usage` reports the
|
|
340
|
+
run's running `{input, output, rounds}` after each round, so a host can
|
|
341
|
+
meter mid-run and `cancel` a run that outruns its budget rather than
|
|
342
|
+
learning the cost only from the terminal `result`. Classified
|
|
343
|
+
failures carry their code from the raise site (`errors[].code` of
|
|
344
|
+
`budget` or `timeout`), so a driver never has to match words in a
|
|
345
|
+
human-readable message.
|
|
346
|
+
|
|
347
|
+
`--max-rounds N` and `--timeout SECONDS` (or the config file's `serve`
|
|
348
|
+
section) bound **each** run rather than the process — the resident
|
|
349
|
+
server grants every `submit` a fresh budget. Both are off by default.
|
|
350
|
+
They are deliberately not fields on the `submit` op: `serve` sandboxes
|
|
351
|
+
untrusted agent code on a host's behalf, so the ceiling belongs to
|
|
352
|
+
whoever starts the sandbox, not to the caller. A tripped budget
|
|
353
|
+
unwinds the agent loop through its normal path, so the run still ends
|
|
354
|
+
with a `result` line carrying `errors[].code` of `budget` or `timeout`
|
|
355
|
+
plus the token usage consumed — a host bills and reports it like any
|
|
356
|
+
other outcome.
|
|
357
|
+
|
|
358
|
+
`timeout` is **cooperative** — checked between rounds, not by a
|
|
359
|
+
watchdog — so it bounds work at those checkpoints but does not
|
|
360
|
+
forcibly interrupt a blocking operation mid-round (a hung tool, a slow
|
|
361
|
+
network read, or a run parked on a mid-run question awaiting a human
|
|
362
|
+
answer). Use `--answer-timeout` to cap how long an ask waits, and rely
|
|
363
|
+
on the hosting process's own exec watchdog (which sends `cancel` then
|
|
364
|
+
kills) as the hard wall-clock stop for anything the cooperative budget
|
|
365
|
+
cannot reach.
|
|
292
366
|
|
|
293
367
|
### Slash commands
|
|
294
368
|
|
|
@@ -306,6 +380,7 @@ protocols with one parser.
|
|
|
306
380
|
| `/clear` | Start a fresh conversation |
|
|
307
381
|
| `/model [name]` | Switch LLM profiles; `default` restores the session's original model |
|
|
308
382
|
| `/agent [name]` | Switch agent system prompt; `default` restores the built-in `agent.md` |
|
|
383
|
+
| `/help` | List the available commands |
|
|
309
384
|
| `/exit` | Quit |
|
|
310
385
|
|
|
311
386
|
Custom commands from `prompts/commands/*.md` are registered as slash commands as well (TUI only).
|
|
@@ -350,12 +425,13 @@ Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux a
|
|
|
350
425
|
make test # unit tests
|
|
351
426
|
venv/bin/pip install -e ".[dev]" # development tools
|
|
352
427
|
venv/bin/ruff check . # lint
|
|
428
|
+
venv/bin/ruff format --check . # formatting (CI blocks on this too)
|
|
353
429
|
venv/bin/pyright # type checking
|
|
354
430
|
venv/bin/python -m build # build sdist + wheel
|
|
355
431
|
venv/bin/pip-audit # dependency audit
|
|
356
432
|
```
|
|
357
433
|
|
|
358
|
-
CI blocks on Ruff and Pyright failures.
|
|
434
|
+
CI blocks on Ruff (lint **and** `format --check`) and Pyright failures.
|
|
359
435
|
|
|
360
436
|
## Design philosophy
|
|
361
437
|
|
|
@@ -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.1"
|
|
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.0 → python_agent_harness-1.5.6.1}/python_agent_harness/core/agent.py
RENAMED
|
@@ -43,7 +43,7 @@ from ..llm.client import LLMClient
|
|
|
43
43
|
from ..session import config
|
|
44
44
|
from ..tools.base import PendingToolResult
|
|
45
45
|
from .context_manager import ContextManager
|
|
46
|
-
from .models import Message, ToolCall
|
|
46
|
+
from .models import Message, ToolCall, display_args
|
|
47
47
|
from .token_estimator import context_window_for, estimate_payload_tokens
|
|
48
48
|
from .tool_runner import (
|
|
49
49
|
NIL_RESULT_PLACEHOLDER, # noqa: F401 (re-exported for backward compat)
|
|
@@ -150,6 +150,16 @@ class AgentLoop:
|
|
|
150
150
|
# from cancellation: a cancelled run with no successor still
|
|
151
151
|
# owns the session and may salvage its partial history.
|
|
152
152
|
self._run_gen = session.run_generation
|
|
153
|
+
# Tag THIS thread (the one that will drive the loop — the run
|
|
154
|
+
# worker, or a sub-agent's own thread) with the run it belongs
|
|
155
|
+
# to, so the session can drop events from a superseded run
|
|
156
|
+
# instead of attributing them to the current one. A detached
|
|
157
|
+
# sub-agent thread can outlive its run; see
|
|
158
|
+
# `Session._emit_superseded`. Suppressed for session doubles
|
|
159
|
+
# that predate the hook.
|
|
160
|
+
mark = getattr(session, "mark_emit_generation", None)
|
|
161
|
+
if callable(mark):
|
|
162
|
+
mark(self._run_gen)
|
|
153
163
|
|
|
154
164
|
def _is_cancelled(self) -> bool:
|
|
155
165
|
"""Whether THIS run must stop (cancelled or superseded).
|
|
@@ -205,14 +215,19 @@ class AgentLoop:
|
|
|
205
215
|
if self.top_level:
|
|
206
216
|
self.error = "Error: round budget exhausted before the run finished"
|
|
207
217
|
self.info["error"] = self.error
|
|
208
|
-
|
|
218
|
+
# The code is assigned HERE, where the outcome is known.
|
|
219
|
+
# A driver reading the result line gets "budget" because
|
|
220
|
+
# the budget check said so — not because a downstream
|
|
221
|
+
# classifier matched words in this sentence, which would
|
|
222
|
+
# silently degrade to "unknown" if it were reworded.
|
|
223
|
+
self.session.notify("error", {"code": "budget", "message": self.error})
|
|
209
224
|
else:
|
|
210
225
|
self.info["budget"] = True
|
|
211
226
|
return True
|
|
212
227
|
if self._deadline_exceeded():
|
|
213
228
|
self.error = f"Error: {_deadline_message(self.timeout)}"
|
|
214
229
|
self.info["error"] = self.error
|
|
215
|
-
self.session.notify("error", self.error)
|
|
230
|
+
self.session.notify("error", {"code": "timeout", "message": self.error})
|
|
216
231
|
return True
|
|
217
232
|
return False
|
|
218
233
|
|
|
@@ -503,8 +518,12 @@ class AgentLoop:
|
|
|
503
518
|
return
|
|
504
519
|
|
|
505
520
|
def safe_delta(text: str) -> None:
|
|
506
|
-
|
|
507
|
-
|
|
521
|
+
# Snapshot: a host that swaps views per run unwires
|
|
522
|
+
# on_delta at run teardown, so re-reading it after the
|
|
523
|
+
# None-test could call None (see Session.notify).
|
|
524
|
+
on_delta = session.on_delta
|
|
525
|
+
if not self._is_cancelled() and on_delta is not None:
|
|
526
|
+
on_delta(text)
|
|
508
527
|
|
|
509
528
|
try:
|
|
510
529
|
# sub-agents are one-shot tasks: they must not see (or
|
|
@@ -581,6 +600,20 @@ class AgentLoop:
|
|
|
581
600
|
totals["input"] = totals.get("input", 0) + usage.input_tokens
|
|
582
601
|
totals["output"] = totals.get("output", 0) + usage.output_tokens
|
|
583
602
|
totals["rounds"] = totals.get("rounds", 0) + 1
|
|
603
|
+
snapshot = {
|
|
604
|
+
"input": totals["input"],
|
|
605
|
+
"output": totals["output"],
|
|
606
|
+
"rounds": totals["rounds"],
|
|
607
|
+
}
|
|
608
|
+
# Publish the running total so a host can meter DURING the
|
|
609
|
+
# run instead of only billing the terminal result line.
|
|
610
|
+
# Without this a run that outlives its user's allowance
|
|
611
|
+
# cannot be stopped until it finishes on its own — the host
|
|
612
|
+
# can now cancel mid-run. Emitted from the top-level loop
|
|
613
|
+
# only (the snapshot already includes sub-agent tokens, so
|
|
614
|
+
# per-sub-agent lines would add noise, not information).
|
|
615
|
+
if self.top_level:
|
|
616
|
+
session.notify("usage", snapshot)
|
|
584
617
|
|
|
585
618
|
# persist the assistant response in the conversation history
|
|
586
619
|
# (text and/or tool calls) so later turns and the UI see it;
|
|
@@ -621,6 +654,15 @@ class AgentLoop:
|
|
|
621
654
|
self.session.last_messages = list(self.messages)
|
|
622
655
|
names = [tc.name for tc in self.pending]
|
|
623
656
|
self.session.notify("tool_start", names)
|
|
657
|
+
# Additive companion event: the same round, but with each
|
|
658
|
+
# call's arguments so a hosting UI can show WHAT is about to
|
|
659
|
+
# run ("Bash(command='ls -la')"), which bare names cannot
|
|
660
|
+
# convey. Kept separate from tool_start so the payload shape
|
|
661
|
+
# of the documented protocol stays backward compatible.
|
|
662
|
+
self.session.notify(
|
|
663
|
+
"tool_calls",
|
|
664
|
+
[{"name": tc.name, "args": display_args(tc.arguments)} for tc in self.pending],
|
|
665
|
+
)
|
|
624
666
|
self._execute_pending()
|
|
625
667
|
|
|
626
668
|
def _handle_tret(self) -> None:
|
{python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/core/models.py
RENAMED
|
@@ -80,6 +80,39 @@ class ToolCall:
|
|
|
80
80
|
elapsed: float | None = None # execution wall-time in seconds (TUI display)
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def arg_repr(value: Any, limit: int = 100) -> str:
|
|
84
|
+
"""repr() of a tool-call argument, truncated to LIMIT chars total.
|
|
85
|
+
|
|
86
|
+
Long values (a long Bash command, a big JSON payload) are cut with
|
|
87
|
+
an ellipsis instead of being dropped from the label entirely.
|
|
88
|
+
"""
|
|
89
|
+
r = repr(value)
|
|
90
|
+
if len(r) <= limit:
|
|
91
|
+
return r
|
|
92
|
+
return r[: limit - 1] + "…"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def display_args(arguments: dict[str, Any] | str, limit: int = 100) -> dict[str, str]:
|
|
96
|
+
"""Tool-call arguments as short display strings, keyed by name.
|
|
97
|
+
|
|
98
|
+
``arguments`` arrives either already decoded or as the raw JSON
|
|
99
|
+
string the model emitted (unparseable when the model truncated it).
|
|
100
|
+
``content`` is skipped: a Write/Edit payload is far too large for a
|
|
101
|
+
one-line label, and it is shown as a diff instead.
|
|
102
|
+
|
|
103
|
+
Values are truncated, so the result is a display artifact -- never
|
|
104
|
+
the data a tool is executed with.
|
|
105
|
+
"""
|
|
106
|
+
if isinstance(arguments, str):
|
|
107
|
+
try:
|
|
108
|
+
arguments = json.loads(arguments)
|
|
109
|
+
except (json.JSONDecodeError, ValueError):
|
|
110
|
+
return {}
|
|
111
|
+
if not isinstance(arguments, dict):
|
|
112
|
+
return {}
|
|
113
|
+
return {k: arg_repr(v, limit) for k, v in arguments.items() if k != "content"}
|
|
114
|
+
|
|
115
|
+
|
|
83
116
|
@dataclass
|
|
84
117
|
class Message:
|
|
85
118
|
"""One conversation message in OpenAI-compatible format.
|
{python_agent_harness-1.5.6.0 → python_agent_harness-1.5.6.1}/python_agent_harness/entry/cli.py
RENAMED
|
@@ -195,6 +195,31 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
195
195
|
session.close()
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
def _resolve_budgets(
|
|
199
|
+
args: argparse.Namespace, cfg_limits: tuple[int | None, float | None]
|
|
200
|
+
) -> tuple[int | None, float | None]:
|
|
201
|
+
"""Resolve ``(max_rounds, timeout)`` for an unattended run.
|
|
202
|
+
|
|
203
|
+
Precedence: an explicit CLI flag wins over the config file's
|
|
204
|
+
section default, which wins over unlimited. A flag of 0/negative
|
|
205
|
+
is an explicit "disable", so it overrides a configured budget
|
|
206
|
+
rather than falling back to it. Shared by ``headless`` and
|
|
207
|
+
``serve``, whose budget flags are identical.
|
|
208
|
+
"""
|
|
209
|
+
cfg_max_rounds, cfg_timeout = cfg_limits
|
|
210
|
+
max_rounds = getattr(args, "max_rounds", None)
|
|
211
|
+
if max_rounds is None:
|
|
212
|
+
max_rounds = cfg_max_rounds
|
|
213
|
+
elif max_rounds <= 0:
|
|
214
|
+
max_rounds = None
|
|
215
|
+
timeout = getattr(args, "timeout", None)
|
|
216
|
+
if timeout is None:
|
|
217
|
+
timeout = cfg_timeout
|
|
218
|
+
elif timeout <= 0:
|
|
219
|
+
timeout = None
|
|
220
|
+
return max_rounds, timeout
|
|
221
|
+
|
|
222
|
+
|
|
198
223
|
def cmd_headless(args: argparse.Namespace) -> int:
|
|
199
224
|
project_dir = getattr(args, "project", None) or os.getcwd()
|
|
200
225
|
session = make_session_with_mcp(
|
|
@@ -211,17 +236,7 @@ def cmd_headless(args: argparse.Namespace) -> int:
|
|
|
211
236
|
runner = run_headless_jsonl if getattr(args, "json", False) else run_headless
|
|
212
237
|
# Unattended budgets: explicit flags win over the config file's
|
|
213
238
|
# headless section (0/negative disables the budget entirely).
|
|
214
|
-
|
|
215
|
-
max_rounds = getattr(args, "max_rounds", None)
|
|
216
|
-
if max_rounds is None:
|
|
217
|
-
max_rounds = cfg_max_rounds
|
|
218
|
-
elif max_rounds <= 0:
|
|
219
|
-
max_rounds = None
|
|
220
|
-
timeout = getattr(args, "timeout", None)
|
|
221
|
-
if timeout is None:
|
|
222
|
-
timeout = cfg_timeout
|
|
223
|
-
elif timeout <= 0:
|
|
224
|
-
timeout = None
|
|
239
|
+
max_rounds, timeout = _resolve_budgets(args, config.load_headless_limits(args.config))
|
|
225
240
|
return runner(
|
|
226
241
|
session,
|
|
227
242
|
prompt,
|
|
@@ -245,9 +260,15 @@ def cmd_serve(args: argparse.Namespace) -> int:
|
|
|
245
260
|
try:
|
|
246
261
|
from .server import run_serve
|
|
247
262
|
|
|
263
|
+
# Per-run budgets: explicit flags win over the config file's
|
|
264
|
+
# serve section (0/negative disables). Applied to EACH submit
|
|
265
|
+
# — the process is resident, so every run gets a fresh budget.
|
|
266
|
+
max_rounds, timeout = _resolve_budgets(args, config.load_serve_limits(args.config))
|
|
248
267
|
return run_serve(
|
|
249
268
|
session,
|
|
250
269
|
answer_timeout=float(getattr(args, "answer_timeout", 0.0) or 0.0),
|
|
270
|
+
max_rounds=max_rounds,
|
|
271
|
+
timeout=timeout,
|
|
251
272
|
)
|
|
252
273
|
finally:
|
|
253
274
|
session.close()
|
|
@@ -344,6 +365,14 @@ def cmd_config(args: argparse.Namespace) -> int:
|
|
|
344
365
|
print(
|
|
345
366
|
f"headless: max_rounds={max_rounds or '(unlimited)'}, timeout={timeout or '(unlimited)'}s"
|
|
346
367
|
)
|
|
368
|
+
serve_rounds, serve_timeout = config.load_serve_limits(args.path)
|
|
369
|
+
if serve_rounds is None and serve_timeout is None:
|
|
370
|
+
print("serve: (unlimited — set serve.max_rounds/timeout or use --max-rounds/--timeout)")
|
|
371
|
+
else:
|
|
372
|
+
print(
|
|
373
|
+
f"serve: max_rounds={serve_rounds or '(unlimited)'}, "
|
|
374
|
+
f"timeout={serve_timeout or '(unlimited)'}s (per run)"
|
|
375
|
+
)
|
|
347
376
|
return 0
|
|
348
377
|
|
|
349
378
|
|
|
@@ -468,6 +497,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
468
497
|
help="give up waiting for a host answer after N seconds (0 = wait "
|
|
469
498
|
'forever, the default; the host can answer via {"op": "answer"})',
|
|
470
499
|
)
|
|
500
|
+
p_serve.add_argument(
|
|
501
|
+
"--max-rounds",
|
|
502
|
+
metavar="N",
|
|
503
|
+
type=int,
|
|
504
|
+
default=None,
|
|
505
|
+
help="cap EACH run at N LLM rounds (per-submit budget, not a "
|
|
506
|
+
"process cap; default: config serve.max_rounds, else unlimited)",
|
|
507
|
+
)
|
|
508
|
+
p_serve.add_argument(
|
|
509
|
+
"--timeout",
|
|
510
|
+
metavar="SECONDS",
|
|
511
|
+
type=float,
|
|
512
|
+
default=None,
|
|
513
|
+
help="wall-clock limit for EACH run in seconds (per-submit budget, "
|
|
514
|
+
"cooperative — checked between rounds, does not interrupt a run "
|
|
515
|
+
"blocked on a mid-run question; default: config serve.timeout, else "
|
|
516
|
+
"unlimited)",
|
|
517
|
+
)
|
|
471
518
|
return parser
|
|
472
519
|
|
|
473
520
|
|
|
@@ -83,6 +83,21 @@ class Controller:
|
|
|
83
83
|
self.session.confirm_fn = view.confirm
|
|
84
84
|
self.session.ask_fn = view.ask
|
|
85
85
|
|
|
86
|
+
def detach_view(self) -> None:
|
|
87
|
+
"""Unwire the session's callbacks (no view attached).
|
|
88
|
+
|
|
89
|
+
The counterpart of ``attach_view``, for a host that swaps views
|
|
90
|
+
per run (``serve``): once a run's view is detached, a worker
|
|
91
|
+
still unwinding from that run emits into nothing instead of
|
|
92
|
+
into whichever view happens to be attached next — its events
|
|
93
|
+
would otherwise be attributed to a different run.
|
|
94
|
+
"""
|
|
95
|
+
self.session.on_delta = None
|
|
96
|
+
self.session.notify_fn = None
|
|
97
|
+
self.session.log_fn = None
|
|
98
|
+
self.session.confirm_fn = None
|
|
99
|
+
self.session.ask_fn = None
|
|
100
|
+
|
|
86
101
|
# ------------------------------------------------------------------
|
|
87
102
|
# read-only state accessors
|
|
88
103
|
# ------------------------------------------------------------------
|