drydock-cli 3.0.105__tar.gz → 3.0.106__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 (119) hide show
  1. {drydock_cli-3.0.105/drydock_cli.egg-info → drydock_cli-3.0.106}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/agent.py +15 -1
  3. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/config.py +5 -0
  4. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/providers.py +36 -9
  5. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/tuning.py +12 -0
  6. {drydock_cli-3.0.105 → drydock_cli-3.0.106/drydock_cli.egg-info}/PKG-INFO +1 -1
  7. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock_cli.egg-info/SOURCES.txt +1 -0
  8. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/pyproject.toml +1 -1
  9. drydock_cli-3.0.106/tests/test_stall_retry.py +53 -0
  10. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/LICENSE +0 -0
  11. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/NOTICE +0 -0
  12. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/README.md +0 -0
  13. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/__init__.py +0 -0
  14. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/__main__.py +0 -0
  15. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/advisor.py +0 -0
  16. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/bash_safety.py +0 -0
  17. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/builtin_skills/__init__.py +0 -0
  18. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/builtin_skills/rmf-categorize.md +0 -0
  19. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/builtin_skills/rmf-control.md +0 -0
  20. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/builtin_skills/rmf-poam.md +0 -0
  21. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/builtin_skills/rmf-review.md +0 -0
  22. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/builtin_skills/stig-assess.md +0 -0
  23. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/builtin_skills/stig-remediate.md +0 -0
  24. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/cci.py +0 -0
  25. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/cli.py +0 -0
  26. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/compaction.py +0 -0
  27. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/detect.py +0 -0
  28. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/extract.py +0 -0
  29. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/gittools.py +0 -0
  30. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/graphrag.py +0 -0
  31. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/guards.py +0 -0
  32. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/loop_detect.py +0 -0
  33. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/mcp.py +0 -0
  34. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/poam.py +0 -0
  35. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/rmf.py +0 -0
  36. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/rmf_graph.py +0 -0
  37. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/skills.py +0 -0
  38. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/stig.py +0 -0
  39. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/tool_registry.py +0 -0
  40. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/tools/__init__.py +0 -0
  41. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/tui/__init__.py +0 -0
  42. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/tui/app.py +0 -0
  43. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/tui/approval.py +0 -0
  44. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/tui/messages.py +0 -0
  45. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/tui/widgets.py +0 -0
  46. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock/web.py +0 -0
  47. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock_cli.egg-info/dependency_links.txt +0 -0
  48. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock_cli.egg-info/entry_points.txt +0 -0
  49. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock_cli.egg-info/requires.txt +0 -0
  50. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/drydock_cli.egg-info/top_level.txt +0 -0
  51. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/setup.cfg +0 -0
  52. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_advisor.py +0 -0
  53. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_approval.py +0 -0
  54. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_back_command.py +0 -0
  55. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_background.py +0 -0
  56. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_binary_output.py +0 -0
  57. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_crossplatform.py +0 -0
  58. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_output_bounding.py +0 -0
  59. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_process_group.py +0 -0
  60. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_safety.py +0 -0
  61. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_sanitize.py +0 -0
  62. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_shell.py +0 -0
  63. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_stdin.py +0 -0
  64. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_stop_partial.py +0 -0
  65. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_timeout_network.py +0 -0
  66. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_bash_timeout_param.py +0 -0
  67. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_cci.py +0 -0
  68. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_cli_agents.py +0 -0
  69. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_compact_command.py +0 -0
  70. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_compaction.py +0 -0
  71. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_config.py +0 -0
  72. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_config_migration.py +0 -0
  73. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_context_limit_config.py +0 -0
  74. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_detect.py +0 -0
  75. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_dispatch.py +0 -0
  76. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_e2e_connected.py +0 -0
  77. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_edit_replace_all.py +0 -0
  78. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_empty_response.py +0 -0
  79. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_extract.py +0 -0
  80. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_failure_loop.py +0 -0
  81. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_first_run_setup.py +0 -0
  82. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_gittools.py +0 -0
  83. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_glob_edit_edges.py +0 -0
  84. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_graphify_example.py +0 -0
  85. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_graphrag.py +0 -0
  86. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_grep_and_read_robust.py +0 -0
  87. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_guards_and_tools.py +0 -0
  88. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_hallucinated_tools.py +0 -0
  89. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_leaked_tool_call.py +0 -0
  90. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_loop_detect.py +0 -0
  91. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_mcp.py +0 -0
  92. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_oneshot_unreachable.py +0 -0
  93. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_plan_autocontinue.py +0 -0
  94. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_poam.py +0 -0
  95. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_providers_unreachable.py +0 -0
  96. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_read_index.py +0 -0
  97. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_rmf.py +0 -0
  98. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_rmf_graph.py +0 -0
  99. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_rmf_stig_graph.py +0 -0
  100. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_runaway_repetition.py +0 -0
  101. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_server_probe.py +0 -0
  102. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_skills.py +0 -0
  103. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_stig.py +0 -0
  104. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_stop.py +0 -0
  105. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_streaming_newlines.py +0 -0
  106. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_subagent.py +0 -0
  107. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_system_prompt_help.py +0 -0
  108. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_todo.py +0 -0
  109. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_tool_arg_coercion.py +0 -0
  110. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_tool_arg_coercion_more.py +0 -0
  111. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_tool_arg_parsing.py +0 -0
  112. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_tools_undo.py +0 -0
  113. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_tui.py +0 -0
  114. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_tuning.py +0 -0
  115. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_viewimage.py +0 -0
  116. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_vision_input.py +0 -0
  117. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_web_tools.py +0 -0
  118. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_write_content_coerce.py +0 -0
  119. {drydock_cli-3.0.105 → drydock_cli-3.0.106}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.105
3
+ Version: 3.0.106
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
@@ -19,7 +19,7 @@ def _plan_has_unfinished(config: dict) -> bool:
19
19
  todo = config.get("_todo")
20
20
  return bool(todo) and any(status != "done" for _, status in todo)
21
21
 
22
- from drydock.providers import stream, AssistantTurn, ReasoningChunk, TextChunk
22
+ from drydock.providers import stream, AssistantTurn, ReasoningChunk, TextChunk, StallRetry
23
23
  from drydock.tool_registry import schemas, execute
24
24
  from drydock.tools import register_all
25
25
 
@@ -159,6 +159,7 @@ def run(
159
159
 
160
160
  # Stream from LLM — with retry on context-length 400 error
161
161
  retries = 0
162
+ stall_retries = 0
162
163
  while retries < 2:
163
164
  try:
164
165
  available = schemas()
@@ -178,6 +179,19 @@ def run(
178
179
  elif isinstance(event, AssistantTurn):
179
180
  assistant_turn = event
180
181
  break # success
182
+ except StallRetry:
183
+ # The local server hung mid-generation (config stall_retry_secs).
184
+ # Re-issue the same request — a fresh generation usually isn't
185
+ # stalled. Bounded so a truly-dead server can't loop forever; on
186
+ # exhaustion end the turn cleanly. Stalls have their OWN budget,
187
+ # separate from the context-error `retries`.
188
+ stall_retries += 1
189
+ if stall_retries > 3:
190
+ yield TextChunk("\n[model server kept stalling — giving up on this step.]\n")
191
+ assistant_turn = None
192
+ break
193
+ yield TextChunk("\n[model server stalled — retrying...]\n")
194
+ continue
181
195
  except Exception as e:
182
196
  err = str(e)
183
197
  if is_context_length_error(err):
@@ -34,6 +34,11 @@ DEFAULTS: dict[str, object] = {
34
34
  # real window BEFORE compacting (the server 400s) — set this to your -c
35
35
  # value. Default 65536 matches the bundled gemma4 server (-c 65536).
36
36
  "context_limit": 65536,
37
+ # Auto-retry a hung model call: if the server produces NOTHING for this many
38
+ # seconds, drydock abandons the wedged request and re-issues it (a fresh
39
+ # generation usually isn't stalled — a known gemma/llama.cpp hang). 0 = off.
40
+ # Set to e.g. 600 on a stall-prone local server. Bounded to a few retries.
41
+ "stall_retry_secs": 0,
37
42
  "theme": "harbor",
38
43
  # Optional SECOND model ("advisor") for a stronger second opinion — e.g. a
39
44
  # Gemini OpenAI-compatible endpoint on another box. Empty = disabled. The
@@ -23,6 +23,13 @@ _LLM_POOL = concurrent.futures.ThreadPoolExecutor(max_workers=8, thread_name_pre
23
23
  class _StopRequested(Exception):
24
24
  """Internal: STOP was pressed during a blocking LLM call."""
25
25
 
26
+
27
+ class StallRetry(Exception):
28
+ """The model server produced no progress for `stall_retry_secs` — the local
29
+ server has hung mid-generation (a known gemma/llama.cpp failure). The agent
30
+ abandons the wedged request and re-issues it; a fresh generation usually
31
+ doesn't stall. Opt-in via config `stall_retry_secs` (0 = off)."""
32
+
26
33
  from drydock.loop_detect import runaway_repetition_len
27
34
  from drydock.tuning import extract_thinking, strip_leaked_tool_calls, strip_thinking_tokens, use_streaming
28
35
 
@@ -138,19 +145,26 @@ def _safe_create(client, kwargs: dict, base_url: str, provider: str, timeout_s:
138
145
 
139
146
 
140
147
  def _create_abortable(client, kwargs: dict, base_url: str, provider: str, cancel,
141
- timeout_s: float = 600.0):
148
+ timeout_s: float = 600.0, stall_secs: float = 0.0):
142
149
  """Like _safe_create, but runs off-thread and polls a cancel Event so STOP
143
150
  can abandon a blocked decode (raising _StopRequested). The orphaned request
144
- runs to completion in the pool thread and its result is dropped."""
151
+ runs to completion in the pool thread and its result is dropped.
152
+
153
+ If stall_secs > 0 and the request produces nothing in that long, raise
154
+ StallRetry so the caller can abandon a hung server and re-issue."""
145
155
  import openai
156
+ import time as _time
146
157
 
147
158
  fut = _LLM_POOL.submit(client.chat.completions.create, **kwargs)
159
+ start = _time.monotonic()
148
160
  while True:
149
161
  try:
150
162
  return fut.result(timeout=0.2)
151
163
  except concurrent.futures.TimeoutError:
152
164
  if cancel is not None and cancel.is_set():
153
165
  raise _StopRequested
166
+ if stall_secs and (_time.monotonic() - start) > stall_secs:
167
+ raise StallRetry(stall_secs) # orphan the wedged request; caller retries
154
168
  # APITimeoutError subclasses APIConnectionError — catch it FIRST.
155
169
  except openai.APITimeoutError as e:
156
170
  raise LLMUnreachable(_friendly_timeout(base_url, timeout_s)) from e
@@ -158,12 +172,17 @@ def _create_abortable(client, kwargs: dict, base_url: str, provider: str, cancel
158
172
  raise LLMUnreachable(_friendly_unreachable(base_url, provider)) from e
159
173
 
160
174
 
161
- def _stream_chunks(response, cancel):
175
+ def _stream_chunks(response, cancel, stall_secs: float = 0.0):
162
176
  """Yield streamed chunks, but pull them via a background thread + queue so a
163
177
  STOP can abandon a read that's blocked waiting for the first token (the
164
178
  thinking phase). On cancel we just stop reading; the orphaned producer
165
- drains in the background."""
179
+ drains in the background.
180
+
181
+ If stall_secs > 0 and no chunk arrives for that long, raise StallRetry — a
182
+ hung server. (A slow-but-alive stream still emits chunks, which reset the
183
+ timer, so this fires only on a true stall.)"""
166
184
  import queue as _queue
185
+ import time as _time
167
186
 
168
187
  q: _queue.Queue = _queue.Queue()
169
188
  _DONE = object()
@@ -178,13 +197,17 @@ def _stream_chunks(response, cancel):
178
197
  q.put(_DONE)
179
198
 
180
199
  _LLM_POOL.submit(_produce)
200
+ last = _time.monotonic()
181
201
  while True:
182
202
  if cancel is not None and cancel.is_set():
183
203
  return
184
204
  try:
185
205
  item = q.get(timeout=0.2)
186
206
  except _queue.Empty:
207
+ if stall_secs and (_time.monotonic() - last) > stall_secs:
208
+ raise StallRetry(stall_secs)
187
209
  continue
210
+ last = _time.monotonic()
188
211
  if item is _DONE:
189
212
  return
190
213
  if isinstance(item, Exception):
@@ -410,6 +433,10 @@ def stream(
410
433
  read_timeout = float(config.get("request_timeout") or 1800.0)
411
434
  except (TypeError, ValueError):
412
435
  read_timeout = 1800.0
436
+ try:
437
+ stall_secs = float(config.get("stall_retry_secs") or 0) # 0 = disabled
438
+ except (TypeError, ValueError):
439
+ stall_secs = 0.0
413
440
  client = OpenAI(
414
441
  api_key=api_key,
415
442
  base_url=base_url,
@@ -461,7 +488,7 @@ def stream(
461
488
  # streaming. If STOP closes the client mid-call the blocking request
462
489
  # raises — swallow that as a clean cancel rather than an error.
463
490
  try:
464
- yield from _complete_nonstreaming(client, kwargs, base_url, provider, cancel, read_timeout)
491
+ yield from _complete_nonstreaming(client, kwargs, base_url, provider, cancel, read_timeout, stall_secs)
465
492
  except _StopRequested:
466
493
  return # STOP — abandon the in-flight request, clean stop
467
494
  except Exception:
@@ -482,11 +509,11 @@ def stream(
482
509
  _rep_checked_at = 0
483
510
 
484
511
  try:
485
- response = _create_abortable(client, kwargs, base_url, provider, cancel, read_timeout)
512
+ response = _create_abortable(client, kwargs, base_url, provider, cancel, read_timeout, stall_secs)
486
513
  except _StopRequested:
487
514
  config.get("_abort", {}).pop("client", None)
488
515
  return
489
- for chunk in _stream_chunks(response, cancel):
516
+ for chunk in _stream_chunks(response, cancel, stall_secs):
490
517
  if not chunk.choices:
491
518
  if hasattr(chunk, "usage") and chunk.usage:
492
519
  in_tok = chunk.usage.prompt_tokens or in_tok
@@ -557,13 +584,13 @@ def stream(
557
584
 
558
585
  def _complete_nonstreaming(
559
586
  client, kwargs: dict, base_url: str = "", provider: str = "", cancel=None,
560
- timeout_s: float = 600.0,
587
+ timeout_s: float = 600.0, stall_secs: float = 0.0,
561
588
  ) -> Generator:
562
589
  """Single non-streaming completion. Yields one TextChunk (if any text)
563
590
  then the AssistantTurn. Used when streaming would corrupt tool-call JSON.
564
591
  Runs off-thread so STOP can abandon a blocked decode.
565
592
  """
566
- resp = _create_abortable(client, kwargs, base_url, provider, cancel, timeout_s)
593
+ resp = _create_abortable(client, kwargs, base_url, provider, cancel, timeout_s, stall_secs)
567
594
  choice = resp.choices[0]
568
595
  msg = choice.message
569
596
 
@@ -91,6 +91,12 @@ _DEFAULT_SYSTEM_PROMPT = (
91
91
  "NOT stop to ask whether to proceed. Call `todo` again to flip a step to "
92
92
  "done and the next to in-progress as you finish each, and only stop when "
93
93
  "every step is done. Don't write a TODO.md file for this.\n"
94
+ "Before you finish, RE-READ the task and confirm EVERY requirement is met — "
95
+ "exact filenames, paths, formats, and values matter, so check them literally. "
96
+ "If the task ships a test/eval/check script (eval.py, test.sh, a make target), "
97
+ "RUN it and fix failures until it passes before you stop. Prefer acting — write "
98
+ "files and run commands — over long planning; don't produce a big plan without "
99
+ "taking a concrete step.\n"
94
100
  "You have VISION: when the user references an image file path (.png/.jpg/"
95
101
  ".jpeg/.gif/.webp/.bmp on disk — a screenshot, mockup, diagram, or photo), it "
96
102
  "is attached and you can SEE it; you can also call the `ViewImage` tool to "
@@ -121,6 +127,12 @@ _GEMMA_SYSTEM_PROMPT = (
121
127
  "steps — do NOT stop to ask whether to proceed. Call `todo` again to update "
122
128
  "it as you finish each step, and only stop when every step is done. Do not "
123
129
  "write a TODO.md file for this.\n"
130
+ "Before you finish, RE-READ the task and confirm EVERY requirement is met — "
131
+ "exact filenames, paths, formats, and values matter, so check them literally. "
132
+ "If the task ships a test/eval/check script (eval.py, test.sh, a make target), "
133
+ "RUN it and fix failures until it passes before you stop. Prefer acting — write "
134
+ "files and run commands — over long planning; don't produce a big plan without "
135
+ "taking a concrete step.\n"
124
136
  "You have VISION: when the user's message references an image file path "
125
137
  "(.png/.jpg/.jpeg/.gif/.webp/.bmp that exists on disk — a screenshot, mockup, "
126
138
  "diagram, or photo), that image is attached and you can SEE it. You can also "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.105
3
+ Version: 3.0.106
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
@@ -97,6 +97,7 @@ tests/test_rmf_stig_graph.py
97
97
  tests/test_runaway_repetition.py
98
98
  tests/test_server_probe.py
99
99
  tests/test_skills.py
100
+ tests/test_stall_retry.py
100
101
  tests/test_stig.py
101
102
  tests/test_stop.py
102
103
  tests/test_streaming_newlines.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.105"
10
+ version = "3.0.106"
11
11
  description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -0,0 +1,53 @@
1
+ """Auto-retry on server stall (config stall_retry_secs, default 0=off): a hung
2
+ model call (server produces nothing for stall_secs) raises StallRetry so the
3
+ agent abandons the wedged request and re-issues it. Plus the self-verify /
4
+ run-test-script / act prompt guidance."""
5
+ from __future__ import annotations
6
+
7
+ import time
8
+
9
+ from drydock.providers import StallRetry, _create_abortable
10
+ from drydock import config
11
+ from drydock.tuning import system_prompt_for_model
12
+
13
+
14
+ class _HangingClient:
15
+ class chat:
16
+ class completions:
17
+ @staticmethod
18
+ def create(**kw):
19
+ time.sleep(60) # simulate a wedged server
20
+
21
+
22
+ def test_stall_retry_raised_on_hang():
23
+ t0 = time.monotonic()
24
+ raised = False
25
+ try:
26
+ _create_abortable(_HangingClient(), {}, "url", "vllm", None,
27
+ timeout_s=600, stall_secs=1)
28
+ except StallRetry:
29
+ raised = True
30
+ assert raised and (time.monotonic() - t0) < 4
31
+
32
+
33
+ def test_stall_retry_disabled_by_default():
34
+ assert config.DEFAULTS.get("stall_retry_secs") == 0
35
+
36
+
37
+ def test_no_stall_when_secs_zero_returns_quickly():
38
+ # stall_secs=0 → never raises StallRetry; a fast create returns normally
39
+ class Fast:
40
+ class chat:
41
+ class completions:
42
+ @staticmethod
43
+ def create(**kw):
44
+ return "ok"
45
+ assert _create_abortable(Fast(), {}, "u", "vllm", None, stall_secs=0) == "ok"
46
+
47
+
48
+ def test_prompts_have_verify_and_act_guidance():
49
+ for m in ("gemma4", "some-other-model"):
50
+ p = system_prompt_for_model(m)
51
+ assert "RE-READ the task" in p
52
+ assert "eval.py" in p and "test.sh" in p # run-the-test-script
53
+ assert "Prefer acting" in p # act, don't over-plan
File without changes
File without changes
File without changes
File without changes