drydock-cli 3.0.31__tar.gz → 3.0.32__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.
- {drydock_cli-3.0.31/drydock_cli.egg-info → drydock_cli-3.0.32}/PKG-INFO +1 -1
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/__init__.py +1 -1
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/agent.py +25 -25
- {drydock_cli-3.0.31 → drydock_cli-3.0.32/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/pyproject.toml +1 -1
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_failure_loop.py +34 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/LICENSE +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/NOTICE +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/README.md +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/cli.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/config.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/detect.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/guards.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/providers.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/setup.cfg +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_config.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_tuning.py +0 -0
|
@@ -115,13 +115,15 @@ def run(
|
|
|
115
115
|
leaked_call_retries = 0
|
|
116
116
|
plan_continue_nudges = 0 # consecutive "you stopped mid-plan" nudges
|
|
117
117
|
empty_response_nudges = 0 # consecutive "you returned nothing" nudges
|
|
118
|
-
# Safety valve for a degenerate loop: the SAME tool call
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
# Safety valve for a degenerate loop: the SAME tool call run over and over
|
|
119
|
+
# with the SAME result — success OR failure (seen failing a Write 160×, and
|
|
120
|
+
# re-running an identical passing `pytest` 92× for 25 min). The advisory
|
|
121
|
+
# loop-note is ignored by weak models, so after a cap we end the turn. Real
|
|
122
|
+
# iterative fixing changes the args, and polling yields a changing result —
|
|
123
|
+
# both reset the streak, so only a truly pointless loop trips it.
|
|
124
|
+
identical_repeat_streak = 0
|
|
125
|
+
last_call_sig = None
|
|
126
|
+
IDENTICAL_REPEAT_CAP = 8
|
|
125
127
|
run_iteration = 0 # stream calls within THIS run() (resets per user message)
|
|
126
128
|
loop_tracker = LoopTracker()
|
|
127
129
|
|
|
@@ -295,16 +297,15 @@ def run(
|
|
|
295
297
|
)
|
|
296
298
|
else:
|
|
297
299
|
result = execute(tc["name"], tc["input"], config)
|
|
298
|
-
# Track byte-identical
|
|
299
|
-
#
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
identical_fail_streak, last_fail_sig = 1, sig
|
|
300
|
+
# Track consecutive byte-identical calls — same name, args AND raw
|
|
301
|
+
# result (captured before annotate prepends its note, which changes
|
|
302
|
+
# each call) — for the safety valve below. A differing result
|
|
303
|
+
# (polling) or differing args (real iteration) resets the streak.
|
|
304
|
+
sig = (tc["name"], str(tc["input"]), result)
|
|
305
|
+
if sig == last_call_sig:
|
|
306
|
+
identical_repeat_streak += 1
|
|
306
307
|
else:
|
|
307
|
-
|
|
308
|
+
identical_repeat_streak, last_call_sig = 1, sig
|
|
308
309
|
# Guide (never block) on exact-repeat tool calls: prepend an
|
|
309
310
|
# advisory note when the same call is made again.
|
|
310
311
|
result = loop_tracker.annotate(tc["name"], tc["input"], result)
|
|
@@ -319,16 +320,15 @@ def run(
|
|
|
319
320
|
"content": result,
|
|
320
321
|
})
|
|
321
322
|
|
|
322
|
-
# Safety valve: the same call has
|
|
323
|
-
# repeating it
|
|
324
|
-
#
|
|
325
|
-
if
|
|
323
|
+
# Safety valve: the same call has run identically (same args AND result)
|
|
324
|
+
# too many times in a row — repeating it changes nothing. End the turn
|
|
325
|
+
# and hand control back rather than burning turns toward MAX_TOOL_TURNS.
|
|
326
|
+
if identical_repeat_streak >= IDENTICAL_REPEAT_CAP:
|
|
326
327
|
yield TextChunk(
|
|
327
|
-
f"\n[Stopped: the same {
|
|
328
|
-
f"{
|
|
329
|
-
"
|
|
330
|
-
"
|
|
331
|
-
"you.]\n"
|
|
328
|
+
f"\n[Stopped: the same {last_call_sig[0]} call ran "
|
|
329
|
+
f"{identical_repeat_streak}× in a row with the same result — "
|
|
330
|
+
"repeating it changes nothing. Control is back to you; tell me "
|
|
331
|
+
"how you'd like to proceed.]\n"
|
|
332
332
|
)
|
|
333
333
|
break
|
|
334
334
|
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.0.
|
|
10
|
+
version = "3.0.32"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.12"
|
|
@@ -41,3 +41,37 @@ def test_changing_args_do_not_trip_valve(monkeypatch):
|
|
|
41
41
|
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
42
42
|
assert any(m.get("role") == "assistant" and m.get("content") == "done"
|
|
43
43
|
for m in st.messages)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_identical_SUCCESS_streak_ends_turn(monkeypatch):
|
|
47
|
+
# The code-red case: a SUCCESSFUL command re-run identically (same args AND
|
|
48
|
+
# same output) — a passing `pytest` 92×. Must also stop, not just failures.
|
|
49
|
+
import tempfile
|
|
50
|
+
calls = {"n": 0}
|
|
51
|
+
|
|
52
|
+
def counting(**kw):
|
|
53
|
+
calls["n"] += 1
|
|
54
|
+
return iter([AssistantTurn(
|
|
55
|
+
"", [{"id": str(calls["n"]), "name": "Bash",
|
|
56
|
+
"input": {"command": "echo hi"}}], 1, 1)])
|
|
57
|
+
|
|
58
|
+
monkeypatch.setattr(agent_mod, "stream", counting)
|
|
59
|
+
st = AgentState()
|
|
60
|
+
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
61
|
+
assert calls["n"] <= 12 # identical success → stopped at the cap, not 200
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_polling_with_changing_result_not_tripped(monkeypatch):
|
|
65
|
+
# Same command, DIFFERENT result each time (a poll) must NOT be stopped —
|
|
66
|
+
# the result changes, so the streak resets. Model finishes on its own.
|
|
67
|
+
import tempfile
|
|
68
|
+
seq = []
|
|
69
|
+
for i in range(3):
|
|
70
|
+
seq.append(AssistantTurn(
|
|
71
|
+
"", [{"id": str(i), "name": "Bash",
|
|
72
|
+
"input": {"command": f"echo {i}"}}], 1, 1)) # different output
|
|
73
|
+
seq.append(AssistantTurn("done", [], 1, 1))
|
|
74
|
+
monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
|
|
75
|
+
st = AgentState()
|
|
76
|
+
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
77
|
+
assert any(m.get("content") == "done" for m in st.messages)
|
|
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
|
|
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
|
|
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
|
|
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
|