drydock-cli 3.0.30__tar.gz → 3.0.31__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.30/drydock_cli.egg-info → drydock_cli-3.0.31}/PKG-INFO +1 -1
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/__init__.py +1 -1
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/agent.py +30 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/pyproject.toml +1 -1
- drydock_cli-3.0.31/tests/test_failure_loop.py +43 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/LICENSE +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/NOTICE +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/README.md +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/cli.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/config.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/detect.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/guards.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/providers.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/setup.cfg +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_config.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_tuning.py +0 -0
|
@@ -115,6 +115,13 @@ 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 FAILING over and
|
|
119
|
+
# over (a weak model that ignores the advisory loop-note — seen failing a
|
|
120
|
+
# Write 160× on a too-large file). Real iterative fixing changes the args,
|
|
121
|
+
# so the streak resets; only byte-identical repeated FAILURES trip it.
|
|
122
|
+
identical_fail_streak = 0
|
|
123
|
+
last_fail_sig = None
|
|
124
|
+
IDENTICAL_FAIL_CAP = 8
|
|
118
125
|
run_iteration = 0 # stream calls within THIS run() (resets per user message)
|
|
119
126
|
loop_tracker = LoopTracker()
|
|
120
127
|
|
|
@@ -288,6 +295,16 @@ def run(
|
|
|
288
295
|
)
|
|
289
296
|
else:
|
|
290
297
|
result = execute(tc["name"], tc["input"], config)
|
|
298
|
+
# Track byte-identical repeated FAILURES (checked before annotate
|
|
299
|
+
# prepends its advisory note) for the safety valve below.
|
|
300
|
+
failed = result.lstrip().startswith(("Error", "REFUSED"))
|
|
301
|
+
sig = (tc["name"], str(tc["input"]))
|
|
302
|
+
if failed and sig == last_fail_sig:
|
|
303
|
+
identical_fail_streak += 1
|
|
304
|
+
elif failed:
|
|
305
|
+
identical_fail_streak, last_fail_sig = 1, sig
|
|
306
|
+
else:
|
|
307
|
+
identical_fail_streak, last_fail_sig = 0, None
|
|
291
308
|
# Guide (never block) on exact-repeat tool calls: prepend an
|
|
292
309
|
# advisory note when the same call is made again.
|
|
293
310
|
result = loop_tracker.annotate(tc["name"], tc["input"], result)
|
|
@@ -302,6 +319,19 @@ def run(
|
|
|
302
319
|
"content": result,
|
|
303
320
|
})
|
|
304
321
|
|
|
322
|
+
# Safety valve: the same call has FAILED identically too many times —
|
|
323
|
+
# repeating it won't help. End the turn and hand control back with a
|
|
324
|
+
# clear note rather than burning turns toward MAX_TOOL_TURNS.
|
|
325
|
+
if identical_fail_streak >= IDENTICAL_FAIL_CAP:
|
|
326
|
+
yield TextChunk(
|
|
327
|
+
f"\n[Stopped: the same {last_fail_sig[0]} call failed "
|
|
328
|
+
f"{identical_fail_streak}× in a row — repeating it won't change "
|
|
329
|
+
"the result. If it's a Write, the file is likely too large for "
|
|
330
|
+
"one response; write it in smaller pieces. Control is back to "
|
|
331
|
+
"you.]\n"
|
|
332
|
+
)
|
|
333
|
+
break
|
|
334
|
+
|
|
305
335
|
# Safe point (all tool results appended): honor a STOP requested while
|
|
306
336
|
# this turn's tools were running, before spending another LLM call.
|
|
307
337
|
if _stopped():
|
|
@@ -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.31"
|
|
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"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Safety valve: a byte-identical tool call that keeps FAILING must end the
|
|
2
|
+
turn after a small cap, not spin toward MAX_TOOL_TURNS (a real session failed
|
|
3
|
+
the same Write 160×)."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import drydock.agent as agent_mod
|
|
7
|
+
from drydock.agent import AgentState, run
|
|
8
|
+
from drydock.providers import AssistantTurn
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_identical_failure_streak_ends_turn(monkeypatch):
|
|
12
|
+
calls = {"n": 0}
|
|
13
|
+
|
|
14
|
+
def counting(**kw):
|
|
15
|
+
calls["n"] += 1
|
|
16
|
+
# Same failing Write every time (empty file_path → Error). id varies but
|
|
17
|
+
# the input (the signature) is identical.
|
|
18
|
+
return iter([AssistantTurn(
|
|
19
|
+
"", [{"id": str(calls["n"]), "name": "Write",
|
|
20
|
+
"input": {"file_path": "", "content": "x"}}], 1, 1)])
|
|
21
|
+
|
|
22
|
+
monkeypatch.setattr(agent_mod, "stream", counting)
|
|
23
|
+
st = AgentState()
|
|
24
|
+
list(run("build it", st, {"model": "m"}, "sys"))
|
|
25
|
+
assert calls["n"] <= 12 # stopped near the cap (8), nowhere near 200
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_changing_args_do_not_trip_valve(monkeypatch):
|
|
29
|
+
# Legit iterative fixing (different args each time) must NOT trip the valve;
|
|
30
|
+
# here each call succeeds (writes a different file), then the model finishes.
|
|
31
|
+
seq = [
|
|
32
|
+
AssistantTurn("", [{"id": "1", "name": "Write",
|
|
33
|
+
"input": {"file_path": "a.txt", "content": "1"}}], 1, 1),
|
|
34
|
+
AssistantTurn("", [{"id": "2", "name": "Write",
|
|
35
|
+
"input": {"file_path": "b.txt", "content": "2"}}], 1, 1),
|
|
36
|
+
AssistantTurn("done", [], 1, 1),
|
|
37
|
+
]
|
|
38
|
+
monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
|
|
39
|
+
st = AgentState()
|
|
40
|
+
import tempfile
|
|
41
|
+
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
42
|
+
assert any(m.get("role") == "assistant" and m.get("content") == "done"
|
|
43
|
+
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
|