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.
Files changed (55) hide show
  1. {drydock_cli-3.0.30/drydock_cli.egg-info → drydock_cli-3.0.31}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/__init__.py +1 -1
  3. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/agent.py +30 -0
  4. {drydock_cli-3.0.30 → drydock_cli-3.0.31/drydock_cli.egg-info}/PKG-INFO +1 -1
  5. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock_cli.egg-info/SOURCES.txt +1 -0
  6. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/pyproject.toml +1 -1
  7. drydock_cli-3.0.31/tests/test_failure_loop.py +43 -0
  8. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/LICENSE +0 -0
  9. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/NOTICE +0 -0
  10. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/README.md +0 -0
  11. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/__main__.py +0 -0
  12. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/bash_safety.py +0 -0
  13. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/cli.py +0 -0
  14. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/compaction.py +0 -0
  15. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/config.py +0 -0
  16. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/detect.py +0 -0
  17. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/guards.py +0 -0
  18. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/loop_detect.py +0 -0
  19. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/providers.py +0 -0
  20. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tool_registry.py +0 -0
  21. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tools/__init__.py +0 -0
  22. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tui/__init__.py +0 -0
  23. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tui/app.py +0 -0
  24. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tui/approval.py +0 -0
  25. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tui/messages.py +0 -0
  26. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tui/widgets.py +0 -0
  27. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock/tuning.py +0 -0
  28. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock_cli.egg-info/dependency_links.txt +0 -0
  29. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock_cli.egg-info/entry_points.txt +0 -0
  30. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock_cli.egg-info/requires.txt +0 -0
  31. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/drydock_cli.egg-info/top_level.txt +0 -0
  32. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/setup.cfg +0 -0
  33. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_approval.py +0 -0
  34. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_back_command.py +0 -0
  35. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_bash_safety.py +0 -0
  36. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_cli_agents.py +0 -0
  37. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_compaction.py +0 -0
  38. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_config.py +0 -0
  39. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_detect.py +0 -0
  40. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_empty_response.py +0 -0
  41. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_first_run_setup.py +0 -0
  42. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_guards_and_tools.py +0 -0
  43. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_hallucinated_tools.py +0 -0
  44. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_leaked_tool_call.py +0 -0
  45. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_loop_detect.py +0 -0
  46. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_plan_autocontinue.py +0 -0
  47. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_providers_unreachable.py +0 -0
  48. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_stop.py +0 -0
  49. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_streaming_newlines.py +0 -0
  50. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_subagent.py +0 -0
  51. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_todo.py +0 -0
  52. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_tool_arg_parsing.py +0 -0
  53. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_tools_undo.py +0 -0
  54. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_tui.py +0 -0
  55. {drydock_cli-3.0.30 → drydock_cli-3.0.31}/tests/test_tuning.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.30
3
+ Version: 3.0.31
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -7,4 +7,4 @@ try:
7
7
  __version__ = _version("drydock-cli")
8
8
  except PackageNotFoundError:
9
9
  # Running from source before install (e.g. PYTHONPATH dev runs).
10
- __version__ = "3.0.30"
10
+ __version__ = "3.0.31"
@@ -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():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.30
3
+ Version: 3.0.31
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -35,6 +35,7 @@ tests/test_compaction.py
35
35
  tests/test_config.py
36
36
  tests/test_detect.py
37
37
  tests/test_empty_response.py
38
+ tests/test_failure_loop.py
38
39
  tests/test_first_run_setup.py
39
40
  tests/test_guards_and_tools.py
40
41
  tests/test_hallucinated_tools.py
@@ -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.30"
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