drydock-cli 3.0.25__tar.gz → 3.0.26__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.25/drydock_cli.egg-info → drydock_cli-3.0.26}/PKG-INFO +1 -1
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/__init__.py +1 -1
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/providers.py +6 -1
- {drydock_cli-3.0.25 → drydock_cli-3.0.26/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/pyproject.toml +1 -1
- drydock_cli-3.0.26/tests/test_streaming_newlines.py +70 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/LICENSE +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/NOTICE +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/README.md +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/agent.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/cli.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/config.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/detect.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/guards.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/setup.cfg +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_config.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.25 → drydock_cli-3.0.26}/tests/test_tuning.py +0 -0
|
@@ -253,7 +253,12 @@ def stream(
|
|
|
253
253
|
if delta.content:
|
|
254
254
|
# Strip any leaked thinking-token markers (best-effort per chunk).
|
|
255
255
|
chunk_text = strip_thinking_tokens(delta.content)
|
|
256
|
-
|
|
256
|
+
# Keep ANY non-empty chunk — including whitespace-only ones. A
|
|
257
|
+
# `.strip()` guard here used to DROP newline-only chunks (the blank
|
|
258
|
+
# line a model streams between markdown blocks arrives as its own
|
|
259
|
+
# chunk), mashing "para### Header" together. `if chunk_text:` still
|
|
260
|
+
# skips chunks that stripping emptied (e.g. a lone thinking marker).
|
|
261
|
+
if chunk_text:
|
|
257
262
|
text += chunk_text
|
|
258
263
|
yield TextChunk(chunk_text)
|
|
259
264
|
|
|
@@ -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.26"
|
|
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,70 @@
|
|
|
1
|
+
"""Streaming must preserve whitespace-only (newline) chunks.
|
|
2
|
+
|
|
3
|
+
A `.strip()` guard in the streaming loop used to DROP a chunk that was only
|
|
4
|
+
"\n\n" — the blank line a model streams between markdown blocks — so a long
|
|
5
|
+
response rendered as "paragraph### Header" mashed together (operator-reported).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import drydock.providers as P
|
|
10
|
+
from drydock.providers import AssistantTurn, TextChunk, stream
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Delta:
|
|
14
|
+
def __init__(self, c):
|
|
15
|
+
self.content = c
|
|
16
|
+
self.tool_calls = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Choice:
|
|
20
|
+
def __init__(self, c):
|
|
21
|
+
self.delta = _Delta(c)
|
|
22
|
+
self.finish_reason = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Chunk:
|
|
26
|
+
def __init__(self, c):
|
|
27
|
+
self.choices = [_Choice(c)]
|
|
28
|
+
self.usage = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_streaming_preserves_newline_only_chunks(monkeypatch):
|
|
32
|
+
chunks = [
|
|
33
|
+
_Chunk("Intro paragraph."),
|
|
34
|
+
_Chunk("\n\n"), # ← blank line between blocks (was dropped)
|
|
35
|
+
_Chunk("### Header"),
|
|
36
|
+
_Chunk("\n\n"),
|
|
37
|
+
_Chunk("body text"),
|
|
38
|
+
]
|
|
39
|
+
monkeypatch.setattr(P, "_safe_create", lambda *a, **k: iter(chunks))
|
|
40
|
+
|
|
41
|
+
full = ""
|
|
42
|
+
streamed = []
|
|
43
|
+
for ev in stream(
|
|
44
|
+
model="gemma4", system="s",
|
|
45
|
+
messages=[{"role": "user", "content": "review"}],
|
|
46
|
+
tool_schemas=[], # no tools → text-only → streaming path even for gemma
|
|
47
|
+
config={"model": "gemma4", "provider": "vllm"},
|
|
48
|
+
):
|
|
49
|
+
if isinstance(ev, TextChunk):
|
|
50
|
+
streamed.append(ev.text)
|
|
51
|
+
elif isinstance(ev, AssistantTurn):
|
|
52
|
+
full = ev.text
|
|
53
|
+
|
|
54
|
+
assert "\n\n" in "".join(streamed) # newline chunks survived streaming
|
|
55
|
+
assert "Intro paragraph.\n\n### Header" in full # blocks not mashed
|
|
56
|
+
assert "### Header\n\nbody text" in full
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_streaming_still_drops_chunk_emptied_by_stripping(monkeypatch):
|
|
60
|
+
# A chunk that is ONLY a thinking marker strips to "" and is skipped (so we
|
|
61
|
+
# don't emit empty text), but real whitespace is kept.
|
|
62
|
+
chunks = [_Chunk("hello"), _Chunk("<|channel>x<channel|>"), _Chunk("\n"), _Chunk("world")]
|
|
63
|
+
monkeypatch.setattr(P, "_safe_create", lambda *a, **k: iter(chunks))
|
|
64
|
+
full = ""
|
|
65
|
+
for ev in stream(model="gemma4", system="s",
|
|
66
|
+
messages=[{"role": "user", "content": "hi"}],
|
|
67
|
+
tool_schemas=[], config={"model": "gemma4", "provider": "vllm"}):
|
|
68
|
+
if isinstance(ev, AssistantTurn):
|
|
69
|
+
full = ev.text
|
|
70
|
+
assert full == "hello\nworld" # marker-only chunk gone, newline kept
|
|
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
|