drydock-cli 3.0.22__tar.gz → 3.0.24__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 (51) hide show
  1. {drydock_cli-3.0.22/drydock_cli.egg-info → drydock_cli-3.0.24}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/__init__.py +1 -1
  3. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/agent.py +18 -0
  4. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/tools/__init__.py +6 -1
  5. {drydock_cli-3.0.22 → drydock_cli-3.0.24/drydock_cli.egg-info}/PKG-INFO +1 -1
  6. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock_cli.egg-info/SOURCES.txt +1 -0
  7. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/pyproject.toml +1 -1
  8. drydock_cli-3.0.24/tests/test_empty_response.py +42 -0
  9. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_guards_and_tools.py +9 -0
  10. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/LICENSE +0 -0
  11. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/NOTICE +0 -0
  12. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/README.md +0 -0
  13. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/__main__.py +0 -0
  14. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/bash_safety.py +0 -0
  15. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/cli.py +0 -0
  16. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/compaction.py +0 -0
  17. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/config.py +0 -0
  18. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/detect.py +0 -0
  19. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/guards.py +0 -0
  20. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/loop_detect.py +0 -0
  21. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/providers.py +0 -0
  22. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/tool_registry.py +0 -0
  23. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/tui/__init__.py +0 -0
  24. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/tui/app.py +0 -0
  25. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/tui/approval.py +0 -0
  26. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/tui/messages.py +0 -0
  27. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/tui/widgets.py +0 -0
  28. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock/tuning.py +0 -0
  29. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock_cli.egg-info/dependency_links.txt +0 -0
  30. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock_cli.egg-info/entry_points.txt +0 -0
  31. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock_cli.egg-info/requires.txt +0 -0
  32. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/drydock_cli.egg-info/top_level.txt +0 -0
  33. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/setup.cfg +0 -0
  34. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_approval.py +0 -0
  35. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_back_command.py +0 -0
  36. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_bash_safety.py +0 -0
  37. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_cli_agents.py +0 -0
  38. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_compaction.py +0 -0
  39. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_config.py +0 -0
  40. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_detect.py +0 -0
  41. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_hallucinated_tools.py +0 -0
  42. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_leaked_tool_call.py +0 -0
  43. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_loop_detect.py +0 -0
  44. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_plan_autocontinue.py +0 -0
  45. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_providers_unreachable.py +0 -0
  46. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_stop.py +0 -0
  47. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_subagent.py +0 -0
  48. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_todo.py +0 -0
  49. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_tools_undo.py +0 -0
  50. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/tests/test_tui.py +0 -0
  51. {drydock_cli-3.0.22 → drydock_cli-3.0.24}/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.22
3
+ Version: 3.0.24
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.22"
10
+ __version__ = "3.0.24"
@@ -114,6 +114,7 @@ def run(
114
114
  session_has_edited = False
115
115
  leaked_call_retries = 0
116
116
  plan_continue_nudges = 0 # consecutive "you stopped mid-plan" nudges
117
+ empty_response_nudges = 0 # consecutive "you returned nothing" nudges
117
118
  run_iteration = 0 # stream calls within THIS run() (resets per user message)
118
119
  loop_tracker = LoopTracker()
119
120
 
@@ -226,6 +227,22 @@ def run(
226
227
  ),
227
228
  })
228
229
  continue
230
+ # A completely EMPTY response (no text, no tool call, no leaked call)
231
+ # is a non-answer, not a deliberate "done" — a weak model sometimes
232
+ # returns nothing on a hard task and the turn dead-ends silently.
233
+ # Nudge ONCE to produce output. Narrow (only empty text) and capped,
234
+ # so unlike a blanket auto-continue it can't loop on real answers.
235
+ if not (assistant_turn.text or "").strip() and empty_response_nudges < 1:
236
+ empty_response_nudges += 1
237
+ state.messages.append({
238
+ "role": "user",
239
+ "content": (
240
+ "[SYSTEM] Your last response was empty. Produce your "
241
+ "result now: either call a tool to do the work, or reply "
242
+ "with text. Do not return an empty message."
243
+ ),
244
+ })
245
+ continue
229
246
  break
230
247
 
231
248
  # Execute each tool call
@@ -282,6 +299,7 @@ def run(
282
299
  # a long, productive plan can run as far as it needs (the cap only
283
300
  # bounds back-to-back stalls).
284
301
  plan_continue_nudges = 0
302
+ empty_response_nudges = 0
285
303
 
286
304
  # Nudge: if past 15 tool calls without any edits, inject gentle guidance
287
305
  if tool_call_count == 15 and not session_has_edited and config.get("force_first_tool"):
@@ -460,7 +460,12 @@ def tool_bash(params: dict, config: dict) -> str:
460
460
  output += f"\n[exit code: {result.returncode}]"
461
461
  return output.strip() or "(no output)"
462
462
  except subprocess.TimeoutExpired:
463
- return f"Error: command timed out after {timeout}s"
463
+ bigger = min(timeout * 4, 600)
464
+ return (
465
+ f"Error: command timed out after {timeout}s. If it is legitimately "
466
+ f"slow (a big query, build, download, or test run), retry with a "
467
+ f"larger timeout — pass timeout: {bigger}. Otherwise it may be hung."
468
+ )
464
469
  except Exception as e:
465
470
  return f"Error: {e}"
466
471
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.22
3
+ Version: 3.0.24
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
@@ -34,6 +34,7 @@ tests/test_cli_agents.py
34
34
  tests/test_compaction.py
35
35
  tests/test_config.py
36
36
  tests/test_detect.py
37
+ tests/test_empty_response.py
37
38
  tests/test_guards_and_tools.py
38
39
  tests/test_hallucinated_tools.py
39
40
  tests/test_leaked_tool_call.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.22"
10
+ version = "3.0.24"
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,42 @@
1
+ """A completely empty assistant response (no text, no tool call, no leaked
2
+ call) should be nudged ONCE to produce output rather than dead-ending the user
3
+ with a silent stop — but it must not loop on real (non-empty) answers."""
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_empty_response_gets_one_nudge(monkeypatch):
12
+ seq = [
13
+ AssistantTurn("", [], 1, 1), # empty → nudge
14
+ AssistantTurn("here is the answer", [], 1, 1), # then a real reply
15
+ ]
16
+ monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
17
+ st = AgentState()
18
+ list(run("do it", st, {"model": "m"}, "sys"))
19
+ nudges = [m for m in st.messages
20
+ if m["role"] == "user" and "response was empty" in m["content"]]
21
+ assert len(nudges) == 1
22
+ assert not seq # both turns consumed
23
+
24
+
25
+ def test_empty_nudge_capped_at_one(monkeypatch):
26
+ # Persistently empty → nudged once, then the loop gives control back.
27
+ seq = [AssistantTurn("", [], 1, 1) for _ in range(6)]
28
+ monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
29
+ st = AgentState()
30
+ list(run("do it", st, {"model": "m"}, "sys"))
31
+ nudges = [m for m in st.messages
32
+ if m["role"] == "user" and "response was empty" in m["content"]]
33
+ assert len(nudges) == 1 # not a spin
34
+
35
+
36
+ def test_nonempty_text_is_not_nudged(monkeypatch):
37
+ seq = [AssistantTurn("all done", [], 1, 1)]
38
+ monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
39
+ st = AgentState()
40
+ list(run("hi", st, {"model": "m"}, "sys"))
41
+ assert not [m for m in st.messages
42
+ if m["role"] == "user" and "response was empty" in m["content"]]
@@ -313,6 +313,15 @@ def test_file_tools_honor_config_cwd(tmp_path):
313
313
  assert "x.py" in tool_bash({"command": "ls sub"}, cfg)
314
314
 
315
315
 
316
+ def test_tool_bash_timeout_suggests_larger_timeout():
317
+ from drydock.tools import tool_bash
318
+ out = tool_bash({"command": "sleep 2", "timeout": 1}, {})
319
+ assert "timed out after 1s" in out
320
+ # Must tell the model it can retry with a bigger timeout (so a legitimately
321
+ # slow query/build doesn't dead-end — what derailed query-optimize).
322
+ assert "timeout: 4" in out
323
+
324
+
316
325
  def test_absolute_paths_pass_through(tmp_path):
317
326
  fp = str(tmp_path / "abs.py")
318
327
  tool_write({"file_path": fp, "content": "a=1\n"}, {"cwd": "/nonexistent"})
File without changes
File without changes
File without changes
File without changes