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.
Files changed (55) hide show
  1. {drydock_cli-3.0.31/drydock_cli.egg-info → drydock_cli-3.0.32}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/__init__.py +1 -1
  3. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/agent.py +25 -25
  4. {drydock_cli-3.0.31 → drydock_cli-3.0.32/drydock_cli.egg-info}/PKG-INFO +1 -1
  5. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/pyproject.toml +1 -1
  6. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_failure_loop.py +34 -0
  7. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/LICENSE +0 -0
  8. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/NOTICE +0 -0
  9. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/README.md +0 -0
  10. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/__main__.py +0 -0
  11. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/bash_safety.py +0 -0
  12. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/cli.py +0 -0
  13. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/compaction.py +0 -0
  14. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/config.py +0 -0
  15. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/detect.py +0 -0
  16. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/guards.py +0 -0
  17. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/loop_detect.py +0 -0
  18. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/providers.py +0 -0
  19. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tool_registry.py +0 -0
  20. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tools/__init__.py +0 -0
  21. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tui/__init__.py +0 -0
  22. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tui/app.py +0 -0
  23. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tui/approval.py +0 -0
  24. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tui/messages.py +0 -0
  25. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tui/widgets.py +0 -0
  26. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock/tuning.py +0 -0
  27. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock_cli.egg-info/SOURCES.txt +0 -0
  28. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock_cli.egg-info/dependency_links.txt +0 -0
  29. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock_cli.egg-info/entry_points.txt +0 -0
  30. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock_cli.egg-info/requires.txt +0 -0
  31. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/drydock_cli.egg-info/top_level.txt +0 -0
  32. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/setup.cfg +0 -0
  33. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_approval.py +0 -0
  34. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_back_command.py +0 -0
  35. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_bash_safety.py +0 -0
  36. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_cli_agents.py +0 -0
  37. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_compaction.py +0 -0
  38. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_config.py +0 -0
  39. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_detect.py +0 -0
  40. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_empty_response.py +0 -0
  41. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_first_run_setup.py +0 -0
  42. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_guards_and_tools.py +0 -0
  43. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_hallucinated_tools.py +0 -0
  44. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_leaked_tool_call.py +0 -0
  45. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_loop_detect.py +0 -0
  46. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_plan_autocontinue.py +0 -0
  47. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_providers_unreachable.py +0 -0
  48. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_stop.py +0 -0
  49. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_streaming_newlines.py +0 -0
  50. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_subagent.py +0 -0
  51. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_todo.py +0 -0
  52. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_tool_arg_parsing.py +0 -0
  53. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_tools_undo.py +0 -0
  54. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/tests/test_tui.py +0 -0
  55. {drydock_cli-3.0.31 → drydock_cli-3.0.32}/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.31
3
+ Version: 3.0.32
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.31"
10
+ __version__ = "3.0.32"
@@ -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 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
+ # 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 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
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
- identical_fail_streak, last_fail_sig = 0, None
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 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:
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 {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"
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.31
3
+ Version: 3.0.32
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,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.31"
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