drydock-cli 3.0.85__tar.gz → 3.0.87__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.85/drydock_cli.egg-info → drydock_cli-3.0.87}/PKG-INFO +1 -1
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/agent.py +2 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/compaction.py +9 -2
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/tui/app.py +20 -1
- {drydock_cli-3.0.85 → drydock_cli-3.0.87/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/pyproject.toml +1 -1
- drydock_cli-3.0.87/tests/test_compact_command.py +91 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_tui.py +26 -0
- drydock_cli-3.0.85/tests/test_compact_command.py +0 -47
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/LICENSE +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/NOTICE +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/README.md +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/cci.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/cli.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/config.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/detect.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/extract.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/guards.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/poam.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/providers.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/skills.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/stig.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock/web.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/setup.cfg +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_config.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.85 → drydock_cli-3.0.87}/tests/test_xccdf.py +0 -0
|
@@ -63,6 +63,7 @@ class AgentState:
|
|
|
63
63
|
messages: list = field(default_factory=list)
|
|
64
64
|
total_input_tokens: int = 0
|
|
65
65
|
total_output_tokens: int = 0
|
|
66
|
+
last_input_tokens: int = 0 # prompt tokens the SERVER counted on the last call
|
|
66
67
|
turn_count: int = 0
|
|
67
68
|
current_effort: str = "" # "high"/"low" of the in-flight LLM call (for the UI)
|
|
68
69
|
|
|
@@ -210,6 +211,7 @@ def run(
|
|
|
210
211
|
|
|
211
212
|
state.total_input_tokens += assistant_turn.input_tokens
|
|
212
213
|
state.total_output_tokens += assistant_turn.output_tokens
|
|
214
|
+
state.last_input_tokens = assistant_turn.input_tokens
|
|
213
215
|
yield TurnDone(assistant_turn.input_tokens, assistant_turn.output_tokens)
|
|
214
216
|
|
|
215
217
|
# No tool calls = conversation complete — UNLESS the model emitted a
|
|
@@ -192,9 +192,16 @@ def emergency_compact(messages: list, context_limit: int = 131072) -> list:
|
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
def maybe_compact(state, config: dict) -> None:
|
|
195
|
-
"""Compact state.messages if approaching context limit.
|
|
195
|
+
"""Compact state.messages if approaching context limit.
|
|
196
|
+
|
|
197
|
+
Uses the MAX of the char-based estimate and the SERVER's real prompt-token
|
|
198
|
+
count from the last call. estimate_tokens (chars/3.5) undercounts token-dense
|
|
199
|
+
content — code, compiler/build output, tracebacks tokenize to ~3 chars/token
|
|
200
|
+
— so on such tasks the real context (what the gauge shows) can pass 60% while
|
|
201
|
+
the estimate is still under it, and compaction fires late. The real count
|
|
202
|
+
catches that."""
|
|
196
203
|
limit = config.get("context_limit", 131072)
|
|
197
|
-
current = estimate_tokens(state.messages)
|
|
204
|
+
current = max(estimate_tokens(state.messages), getattr(state, "last_input_tokens", 0))
|
|
198
205
|
|
|
199
206
|
if current > limit * 0.60:
|
|
200
207
|
state.messages = compact(state.messages, limit)
|
|
@@ -59,6 +59,12 @@ BANNER = (
|
|
|
59
59
|
# Animated "working" line: a turning-helm arc spinner + a nautical gerund +
|
|
60
60
|
# elapsed time + streamed-token count. Rendered in-line above the status bar.
|
|
61
61
|
_SPINNER = "◜◠◝◞◡◟"
|
|
62
|
+
# No agent event for this long → the activity line warns the model may be stalled
|
|
63
|
+
# (the gemma llama.cpp server can hang mid-generation on hard prompts). Advisory
|
|
64
|
+
# only. Set above the longest LEGIT silent stretch seen (a ~135s high-effort
|
|
65
|
+
# think before the first tool call) so normal slow turns don't nag; observed
|
|
66
|
+
# stalls ran 3–5 min+, so 180s still flags them well before the 30-min timeout.
|
|
67
|
+
_STALL_HINT_SECS = 180
|
|
62
68
|
_WORKING_WORDS = [
|
|
63
69
|
"Battening", "Splicing", "Hoisting", "Heaving", "Trimming", "Tacking",
|
|
64
70
|
"Mooring", "Charting", "Navigating", "Sounding", "Caulking", "Rigging",
|
|
@@ -262,6 +268,7 @@ class DrydockApp(App):
|
|
|
262
268
|
self._work_start = 0.0
|
|
263
269
|
self._work_word = ""
|
|
264
270
|
self._work_chars = 0 # streamed output chars this turn (→ ~tokens)
|
|
271
|
+
self._last_progress = 0.0 # monotonic time of the last agent event (stall watchdog)
|
|
265
272
|
self._spinner_i = 0
|
|
266
273
|
|
|
267
274
|
# The agent (worker thread) calls this to gate a sensitive command on
|
|
@@ -349,7 +356,13 @@ class DrydockApp(App):
|
|
|
349
356
|
effort = self.state.current_effort
|
|
350
357
|
eff = f" · thinking with {effort} effort" if effort else ""
|
|
351
358
|
queued = f" · {len(self._queue)} queued" if self._queue else ""
|
|
352
|
-
|
|
359
|
+
# Stall watchdog (advisory): if no agent event has arrived for a while,
|
|
360
|
+
# the model server may be hung. Flag it so the user isn't left guessing
|
|
361
|
+
# whether it's thinking or stalled — but never act (Esc is theirs).
|
|
362
|
+
silent = time.monotonic() - self._last_progress if self._last_progress else 0.0
|
|
363
|
+
stall = (f" ⚠ no output for {int(silent)}s — the model may be slow or stalled; "
|
|
364
|
+
"Esc to stop") if silent > _STALL_HINT_SECS else ""
|
|
365
|
+
return f"{spin} {self._work_word}… ({elapsed} · ↓ {toks} tokens{eff}{queued}){stall}"
|
|
353
366
|
|
|
354
367
|
def _refresh_status(self) -> None:
|
|
355
368
|
# The 0.18s _tick_work timer can fire one last time DURING app teardown,
|
|
@@ -435,6 +448,7 @@ class DrydockApp(App):
|
|
|
435
448
|
self.query_one("#todo", Static).update("")
|
|
436
449
|
self._busy = True
|
|
437
450
|
self._work_start = time.monotonic()
|
|
451
|
+
self._last_progress = self._work_start
|
|
438
452
|
self._work_word = random.choice(_WORKING_WORDS)
|
|
439
453
|
self._work_chars = 0
|
|
440
454
|
self._spinner_i = 0
|
|
@@ -1133,6 +1147,11 @@ class DrydockApp(App):
|
|
|
1133
1147
|
def _run_agent(self, text: str) -> None:
|
|
1134
1148
|
try:
|
|
1135
1149
|
for ev in run(text, self.state, self.config, self.system):
|
|
1150
|
+
# Any event = the model/server is making progress → reset the
|
|
1151
|
+
# stall watchdog. During a real stall the provider stream blocks
|
|
1152
|
+
# and NO event arrives, so this timestamp freezes and
|
|
1153
|
+
# _working_text surfaces a hint.
|
|
1154
|
+
self._last_progress = time.monotonic()
|
|
1136
1155
|
if isinstance(ev, ReasoningChunk):
|
|
1137
1156
|
self.post_message(AgentReasoning(ev.text))
|
|
1138
1157
|
elif isinstance(ev, TextChunk):
|
|
@@ -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.87"
|
|
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,91 @@
|
|
|
1
|
+
"""The /compact slash command (advertised in /help) must actually compact:
|
|
2
|
+
wire it to compaction.compact and report tokens freed. Regression for the gap
|
|
3
|
+
where /help listed /compact but handle_command never implemented it."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from drydock import cli
|
|
7
|
+
from drydock.agent import AgentState
|
|
8
|
+
from drydock.compaction import estimate_tokens
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _state_with_big_tool_result() -> AgentState:
|
|
12
|
+
state = AgentState()
|
|
13
|
+
state.messages = [
|
|
14
|
+
{"role": "user", "content": "do a thing"},
|
|
15
|
+
{
|
|
16
|
+
"role": "assistant",
|
|
17
|
+
"content": "",
|
|
18
|
+
"tool_calls": [{"id": "c1", "name": "Bash", "input": {"command": "cat big"}}],
|
|
19
|
+
},
|
|
20
|
+
{"role": "tool", "tool_call_id": "c1", "content": "Z" * 8000},
|
|
21
|
+
{"role": "assistant", "content": "done"},
|
|
22
|
+
]
|
|
23
|
+
return state
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_compact_command_is_handled():
|
|
27
|
+
"""/compact returns True (handled), not False (falls through to the agent)."""
|
|
28
|
+
state = AgentState()
|
|
29
|
+
assert cli.handle_command("/compact", state, {"context_limit": 65536}) is True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_compact_shrinks_a_bloated_conversation(capsys):
|
|
33
|
+
state = _state_with_big_tool_result()
|
|
34
|
+
before = estimate_tokens(state.messages)
|
|
35
|
+
|
|
36
|
+
cli.handle_command("/compact", state, {"context_limit": 65536})
|
|
37
|
+
|
|
38
|
+
after = estimate_tokens(state.messages)
|
|
39
|
+
assert after < before # the 8000-char tool result was truncated
|
|
40
|
+
out = capsys.readouterr().out.lower()
|
|
41
|
+
assert "compact" in out and "freed" in out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_compact_on_empty_conversation_is_a_noop(capsys):
|
|
45
|
+
state = AgentState()
|
|
46
|
+
assert cli.handle_command("/compact", state, {"context_limit": 65536}) is True
|
|
47
|
+
assert "nothing to compact" in capsys.readouterr().out.lower()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_maybe_compact_uses_real_token_count_not_just_estimate():
|
|
51
|
+
"""maybe_compact must trigger on the SERVER's real prompt-token count, not
|
|
52
|
+
only the chars/3.5 estimate (which undercounts dense code/build output).
|
|
53
|
+
Regression: build task hit 64% real ctx without compacting because the
|
|
54
|
+
estimate read ~55%."""
|
|
55
|
+
from drydock.agent import AgentState
|
|
56
|
+
from drydock.compaction import maybe_compact, estimate_tokens
|
|
57
|
+
|
|
58
|
+
st = AgentState()
|
|
59
|
+
# A few small messages: the char estimate is tiny (well under 60% of 65536)…
|
|
60
|
+
st.messages = [
|
|
61
|
+
{"role": "user", "content": "hi"},
|
|
62
|
+
{"role": "assistant", "content": "hello", "tool_calls": []},
|
|
63
|
+
]
|
|
64
|
+
assert estimate_tokens(st.messages) < 0.60 * 65536 # estimate says "fine"
|
|
65
|
+
cfg = {"context_limit": 65536}
|
|
66
|
+
# …but the server reported a large real prompt (dense content) → must compact.
|
|
67
|
+
st.last_input_tokens = 45000 # 68% of 65536
|
|
68
|
+
before = list(st.messages)
|
|
69
|
+
maybe_compact(st, cfg)
|
|
70
|
+
# compact() ran (it targets 60%); with only tiny messages it can't shrink, but
|
|
71
|
+
# the point is it was INVOKED on the real count — verify the trigger path by
|
|
72
|
+
# checking a case with droppable content:
|
|
73
|
+
st.messages = before + [
|
|
74
|
+
{"role": "assistant", "content": "", "tool_calls": [{"id": "a", "name": "Bash", "input": {"command": "x"}}]},
|
|
75
|
+
{"role": "tool", "tool_call_id": "a", "name": "Bash", "content": "Z" * 200000}, # huge old tool result
|
|
76
|
+
]
|
|
77
|
+
st.last_input_tokens = 45000
|
|
78
|
+
maybe_compact(st, cfg)
|
|
79
|
+
# the huge old tool result should be trimmed by compact()
|
|
80
|
+
assert estimate_tokens(st.messages) < estimate_tokens(before + [{"role": "tool", "content": "Z" * 200000}])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_maybe_compact_low_real_count_no_trigger():
|
|
84
|
+
from drydock.agent import AgentState
|
|
85
|
+
from drydock.compaction import maybe_compact
|
|
86
|
+
st = AgentState()
|
|
87
|
+
st.messages = [{"role": "user", "content": "small"}]
|
|
88
|
+
st.last_input_tokens = 1000
|
|
89
|
+
before = list(st.messages)
|
|
90
|
+
maybe_compact(st, {"context_limit": 65536})
|
|
91
|
+
assert st.messages == before # nothing to do, unchanged
|
|
@@ -457,3 +457,29 @@ def test_ask_bang_injects_advice_but_plain_ask_does_not():
|
|
|
457
457
|
assert started == []
|
|
458
458
|
|
|
459
459
|
asyncio.run(main())
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_stall_watchdog_hint_appears_after_silence():
|
|
463
|
+
"""The activity line warns when no agent event has arrived for a while
|
|
464
|
+
(a hung model server) — advisory, and it clears once progress resumes."""
|
|
465
|
+
import time as _time
|
|
466
|
+
from drydock.tui.app import _STALL_HINT_SECS
|
|
467
|
+
|
|
468
|
+
async def main():
|
|
469
|
+
app = DrydockApp({"model": "gemma4", "provider": "vllm", "cwd": "/tmp"})
|
|
470
|
+
async with app.run_test() as pilot:
|
|
471
|
+
await pilot.pause()
|
|
472
|
+
app._busy = True
|
|
473
|
+
app._work_start = _time.monotonic()
|
|
474
|
+
app._work_word = "Working"
|
|
475
|
+
# fresh progress → no hint
|
|
476
|
+
app._last_progress = _time.monotonic()
|
|
477
|
+
assert "stalled" not in app._working_text()
|
|
478
|
+
# simulate silence beyond the threshold → hint appears
|
|
479
|
+
app._last_progress = _time.monotonic() - (_STALL_HINT_SECS + 5)
|
|
480
|
+
assert "stalled" in app._working_text() and "Esc to stop" in app._working_text()
|
|
481
|
+
# progress resumes → hint clears
|
|
482
|
+
app._last_progress = _time.monotonic()
|
|
483
|
+
assert "stalled" not in app._working_text()
|
|
484
|
+
|
|
485
|
+
asyncio.run(main())
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"""The /compact slash command (advertised in /help) must actually compact:
|
|
2
|
-
wire it to compaction.compact and report tokens freed. Regression for the gap
|
|
3
|
-
where /help listed /compact but handle_command never implemented it."""
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
from drydock import cli
|
|
7
|
-
from drydock.agent import AgentState
|
|
8
|
-
from drydock.compaction import estimate_tokens
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _state_with_big_tool_result() -> AgentState:
|
|
12
|
-
state = AgentState()
|
|
13
|
-
state.messages = [
|
|
14
|
-
{"role": "user", "content": "do a thing"},
|
|
15
|
-
{
|
|
16
|
-
"role": "assistant",
|
|
17
|
-
"content": "",
|
|
18
|
-
"tool_calls": [{"id": "c1", "name": "Bash", "input": {"command": "cat big"}}],
|
|
19
|
-
},
|
|
20
|
-
{"role": "tool", "tool_call_id": "c1", "content": "Z" * 8000},
|
|
21
|
-
{"role": "assistant", "content": "done"},
|
|
22
|
-
]
|
|
23
|
-
return state
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_compact_command_is_handled():
|
|
27
|
-
"""/compact returns True (handled), not False (falls through to the agent)."""
|
|
28
|
-
state = AgentState()
|
|
29
|
-
assert cli.handle_command("/compact", state, {"context_limit": 65536}) is True
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_compact_shrinks_a_bloated_conversation(capsys):
|
|
33
|
-
state = _state_with_big_tool_result()
|
|
34
|
-
before = estimate_tokens(state.messages)
|
|
35
|
-
|
|
36
|
-
cli.handle_command("/compact", state, {"context_limit": 65536})
|
|
37
|
-
|
|
38
|
-
after = estimate_tokens(state.messages)
|
|
39
|
-
assert after < before # the 8000-char tool result was truncated
|
|
40
|
-
out = capsys.readouterr().out.lower()
|
|
41
|
-
assert "compact" in out and "freed" in out
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def test_compact_on_empty_conversation_is_a_noop(capsys):
|
|
45
|
-
state = AgentState()
|
|
46
|
-
assert cli.handle_command("/compact", state, {"context_limit": 65536}) is True
|
|
47
|
-
assert "nothing to compact" in capsys.readouterr().out.lower()
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|