drydock-cli 3.0.86__tar.gz → 3.0.88__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.86/drydock_cli.egg-info → drydock_cli-3.0.88}/PKG-INFO +1 -1
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/agent.py +2 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/compaction.py +9 -2
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/tools/__init__.py +1 -1
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/tuning.py +7 -3
- {drydock_cli-3.0.86 → drydock_cli-3.0.88/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/pyproject.toml +1 -1
- drydock_cli-3.0.88/tests/test_compact_command.py +91 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_viewimage.py +12 -0
- drydock_cli-3.0.86/tests/test_compact_command.py +0 -47
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/LICENSE +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/NOTICE +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/README.md +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/cci.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/cli.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/config.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/detect.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/extract.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/guards.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/poam.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/providers.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/skills.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/stig.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock/web.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/setup.cfg +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_config.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.86 → drydock_cli-3.0.88}/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)
|
|
@@ -85,7 +85,7 @@ SCHEMAS = [
|
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
"name": "ViewImage",
|
|
88
|
-
"description": "Look at an image file with your vision — use this when you need to SEE a screenshot, mockup, diagram, photo, or rendered output (.png/.jpg/.jpeg/.gif/.webp/.bmp). The image is shown to you so you can describe it, read text from it, or debug it. (Reading an image with the Read tool gives binary garbage; use ViewImage instead.)",
|
|
88
|
+
"description": "Look at an image file with your vision — use this when you need to SEE a screenshot, mockup, diagram, photo, or rendered output (.png/.jpg/.jpeg/.gif/.webp/.bmp). The image is shown to you so you can describe it, read text from it, or debug it. To READ TEXT OR DATA out of an image — a scanned document, invoice, receipt, form, or a screenshot with text — use ViewImage FIRST (you can read it directly); prefer it over OCR tools like tesseract/pdftotext, which are often unreliable or not installed. (Reading an image with the Read tool gives binary garbage; use ViewImage instead.)",
|
|
89
89
|
"input_schema": {
|
|
90
90
|
"type": "object",
|
|
91
91
|
"properties": {
|
|
@@ -95,7 +95,9 @@ _DEFAULT_SYSTEM_PROMPT = (
|
|
|
95
95
|
".jpeg/.gif/.webp/.bmp on disk — a screenshot, mockup, diagram, or photo), it "
|
|
96
96
|
"is attached and you can SEE it; you can also call the `ViewImage` tool to "
|
|
97
97
|
"look at any image you find yourself (Read gives binary garbage for images — "
|
|
98
|
-
"use ViewImage).
|
|
98
|
+
"use ViewImage). To read text/data out of a scanned document, invoice, "
|
|
99
|
+
"receipt, or screenshot, use ViewImage directly rather than OCR tools like "
|
|
100
|
+
"tesseract. Describe it, read text from it, or debug with it — never "
|
|
99
101
|
"claim you cannot view images."
|
|
100
102
|
)
|
|
101
103
|
|
|
@@ -123,8 +125,10 @@ _GEMMA_SYSTEM_PROMPT = (
|
|
|
123
125
|
"(.png/.jpg/.jpeg/.gif/.webp/.bmp that exists on disk — a screenshot, mockup, "
|
|
124
126
|
"diagram, or photo), that image is attached and you can SEE it. You can also "
|
|
125
127
|
"call the `ViewImage` tool to look at any image you find yourself (Read gives "
|
|
126
|
-
"binary garbage for images — use ViewImage).
|
|
127
|
-
"
|
|
128
|
+
"binary garbage for images — use ViewImage). To read TEXT or DATA out of a "
|
|
129
|
+
"scanned document, invoice, receipt, form, or a screenshot, use ViewImage "
|
|
130
|
+
"directly — don't reach for OCR tools like tesseract first. Describe it, read "
|
|
131
|
+
"text from it, or use it to debug — never claim you cannot view images."
|
|
128
132
|
)
|
|
129
133
|
|
|
130
134
|
|
|
@@ -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.88"
|
|
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
|
|
@@ -74,3 +74,15 @@ def test_image_load_error_classifier():
|
|
|
74
74
|
assert is_image_load_error("invalid_request_error: could not decode image")
|
|
75
75
|
assert not is_image_load_error("rate limit exceeded")
|
|
76
76
|
assert not is_image_load_error("context_length_exceeded")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_viewimage_nudges_over_ocr_for_documents():
|
|
80
|
+
"""The tool description + both system prompts steer the model to ViewImage
|
|
81
|
+
(its own vision) for reading text off invoices/receipts/scans, instead of
|
|
82
|
+
reaching for OCR tools first (observed on the financial-doc tbench task)."""
|
|
83
|
+
from drydock.tuning import system_prompt_for_model
|
|
84
|
+
desc = reg.get("ViewImage").schema["description"]
|
|
85
|
+
assert "OCR" in desc and ("invoice" in desc or "receipt" in desc)
|
|
86
|
+
for model in ("gemma4", "some-other-model"):
|
|
87
|
+
p = system_prompt_for_model(model)
|
|
88
|
+
assert "OCR" in p and "ViewImage" in p
|
|
@@ -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
|