drydock-cli 3.0.114__tar.gz → 3.0.115__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.114/drydock_cli.egg-info → drydock_cli-3.0.115}/PKG-INFO +1 -1
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/agent.py +14 -10
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/providers.py +28 -4
- {drydock_cli-3.0.114 → drydock_cli-3.0.115/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock_cli.egg-info/SOURCES.txt +1 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/pyproject.toml +1 -1
- drydock_cli-3.0.115/tests/test_repetition_interrupt.py +68 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/LICENSE +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/NOTICE +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/README.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/cci.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/cli.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/config.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/detect.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/extract.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/guards.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/poam.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/skills.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/stig.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock/web.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/setup.cfg +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_config.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.114 → drydock_cli-3.0.115}/tests/test_xccdf.py +0 -0
|
@@ -33,7 +33,7 @@ def _plan_has_unfinished(config: dict) -> bool:
|
|
|
33
33
|
todo = config.get("_todo")
|
|
34
34
|
return bool(todo) and any(status != "done" for _, status in todo)
|
|
35
35
|
|
|
36
|
-
from drydock.providers import stream, AssistantTurn, ReasoningChunk, TextChunk, StallRetry
|
|
36
|
+
from drydock.providers import stream, AssistantTurn, ReasoningChunk, TextChunk, StallRetry, RepetitionDetected
|
|
37
37
|
from drydock.tool_registry import schemas, execute
|
|
38
38
|
from drydock.tools import register_all
|
|
39
39
|
|
|
@@ -194,24 +194,28 @@ def run(
|
|
|
194
194
|
elif isinstance(event, AssistantTurn):
|
|
195
195
|
assistant_turn = event
|
|
196
196
|
break # success
|
|
197
|
-
except StallRetry:
|
|
198
|
-
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
197
|
+
except StallRetry as _sr:
|
|
198
|
+
# Stalled/over-thought past stall_retry_secs (wall-time), OR collapsed
|
|
199
|
+
# into a pure-repetition loop (RepetitionDetected, content-based). A
|
|
200
|
+
# first wall-time retry re-issues as-is (a transient hang clears); a
|
|
201
|
+
# persistent stall OR any repetition loop goes straight to decisive
|
|
202
|
+
# mode — a forcing suffix + hard token cap that make a long reasoning
|
|
203
|
+
# turn impossible. Repetition escalates immediately (re-issuing a loop
|
|
204
|
+
# just loops again). Bounded; on exhaustion end cleanly.
|
|
205
|
+
is_rep = isinstance(_sr, RepetitionDetected)
|
|
203
206
|
stall_retries += 1
|
|
204
207
|
if stall_retries > 3:
|
|
205
|
-
yield TextChunk("\n[model kept stalling/
|
|
208
|
+
yield TextChunk("\n[model kept stalling/looping — giving up on this step.]\n")
|
|
206
209
|
assistant_turn = None
|
|
207
210
|
break
|
|
208
|
-
if stall_retries >= 2 and not decisive:
|
|
211
|
+
if (is_rep or stall_retries >= 2) and not decisive:
|
|
209
212
|
decisive = True
|
|
210
213
|
turn_config = dict(turn_config)
|
|
211
214
|
cur = int(turn_config.get("max_tokens", 8192) or 8192)
|
|
212
215
|
turn_config["max_tokens"] = min(cur, _DECISIVE_MAX_TOKENS)
|
|
213
216
|
turn_config["reasoning_effort"] = "low"
|
|
214
|
-
|
|
217
|
+
why = "output looping" if is_rep else "taking too long"
|
|
218
|
+
yield TextChunk(f"\n[{why} — forcing a decisive, single-action step...]\n")
|
|
215
219
|
else:
|
|
216
220
|
yield TextChunk("\n[model server stalled — retrying...]\n")
|
|
217
221
|
continue
|
|
@@ -30,6 +30,14 @@ class StallRetry(Exception):
|
|
|
30
30
|
abandons the wedged request and re-issues it; a fresh generation usually
|
|
31
31
|
doesn't stall. Opt-in via config `stall_retry_secs` (0 = off)."""
|
|
32
32
|
|
|
33
|
+
|
|
34
|
+
class RepetitionDetected(StallRetry):
|
|
35
|
+
"""CONTENT trigger for the over-think interrupt: the model collapsed into a
|
|
36
|
+
pure-repetition loop (same short unit ×6+, 600+ chars) and produced no usable
|
|
37
|
+
action. Unlike the wall-time StallRetry this fires on the *content*, so it
|
|
38
|
+
never trips on productive-but-long reasoning — only genuine spinning. A plain
|
|
39
|
+
re-issue would just loop again, so the agent goes straight to decisive mode."""
|
|
40
|
+
|
|
33
41
|
from drydock.loop_detect import runaway_repetition_len
|
|
34
42
|
from drydock.tuning import extract_thinking, strip_leaked_tool_calls, strip_thinking_tokens, use_streaming
|
|
35
43
|
|
|
@@ -540,6 +548,11 @@ def stream(
|
|
|
540
548
|
_rep_checked_at = len(text)
|
|
541
549
|
run = runaway_repetition_len(text)
|
|
542
550
|
if run:
|
|
551
|
+
# With the over-think interrupt on, a genuine loop escalates
|
|
552
|
+
# to a decisive retry (recover) instead of returning a
|
|
553
|
+
# truncated turn. Otherwise keep the advisory trim+stop.
|
|
554
|
+
if stall_secs:
|
|
555
|
+
raise RepetitionDetected(0)
|
|
543
556
|
text = (
|
|
544
557
|
text[: len(text) - run]
|
|
545
558
|
+ "\n[… output began repeating — stopped by drydock]"
|
|
@@ -599,10 +612,6 @@ def _complete_nonstreaming(
|
|
|
599
612
|
# the generic special-token pass would eat the tool_call markers.
|
|
600
613
|
text, had_leak = strip_leaked_tool_calls(msg.content or "")
|
|
601
614
|
thinking, text = extract_thinking(text)
|
|
602
|
-
if thinking:
|
|
603
|
-
yield ReasoningChunk(thinking)
|
|
604
|
-
if text.strip():
|
|
605
|
-
yield TextChunk(text)
|
|
606
615
|
|
|
607
616
|
tool_calls = []
|
|
608
617
|
for i, tc in enumerate(getattr(msg, "tool_calls", None) or []):
|
|
@@ -613,6 +622,21 @@ def _complete_nonstreaming(
|
|
|
613
622
|
"input": inp,
|
|
614
623
|
})
|
|
615
624
|
|
|
625
|
+
# Repetition trigger for the over-think interrupt (gated by the same
|
|
626
|
+
# stall_secs feature). Non-streaming gemma turns are where the over-think
|
|
627
|
+
# lives; if the turn collapsed into a pure loop and produced NO usable action,
|
|
628
|
+
# go decisive rather than feeding the garbage turn into the conversation. Only
|
|
629
|
+
# fires on genuine repetition, so productive-but-long reasoning is never hit.
|
|
630
|
+
if stall_secs and not tool_calls and (
|
|
631
|
+
runaway_repetition_len(thinking or "") or runaway_repetition_len(text or "")
|
|
632
|
+
):
|
|
633
|
+
raise RepetitionDetected(0)
|
|
634
|
+
|
|
635
|
+
if thinking:
|
|
636
|
+
yield ReasoningChunk(thinking)
|
|
637
|
+
if text.strip():
|
|
638
|
+
yield TextChunk(text)
|
|
639
|
+
|
|
616
640
|
usage = getattr(resp, "usage", None)
|
|
617
641
|
in_tok = usage.prompt_tokens if usage else 0
|
|
618
642
|
out_tok = usage.completion_tokens if usage else 0
|
|
@@ -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.115"
|
|
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,68 @@
|
|
|
1
|
+
"""Content-based repetition trigger for the over-think interrupt: a non-streaming
|
|
2
|
+
turn that collapses into a pure loop AND produces no action escalates straight to
|
|
3
|
+
decisive mode. It must NOT fire on varied reasoning, on a turn with a tool call,
|
|
4
|
+
or when the interrupt is disabled (stall_secs=0)."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from drydock import agent, providers
|
|
10
|
+
from drydock.providers import AssistantTurn, RepetitionDetected, _complete_nonstreaming
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Fn:
|
|
14
|
+
def __init__(self, name, args): self.name = name; self.arguments = args
|
|
15
|
+
class _TC:
|
|
16
|
+
def __init__(self, name, args): self.id = "c1"; self.function = _Fn(name, args)
|
|
17
|
+
class _Msg:
|
|
18
|
+
def __init__(self, content, tcs=None): self.content = content; self.tool_calls = tcs or []
|
|
19
|
+
class _Choice:
|
|
20
|
+
def __init__(self, msg): self.message = msg
|
|
21
|
+
class _Resp:
|
|
22
|
+
def __init__(self, content, tcs=None): self.choices = [_Choice(_Msg(content, tcs))]; self.usage = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _patch(monkeypatch, content, tcs=None):
|
|
26
|
+
monkeypatch.setattr(providers, "_create_abortable", lambda *a, **k: _Resp(content, tcs))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_pure_loop_no_action_raises(monkeypatch):
|
|
30
|
+
_patch(monkeypatch, "loop " * 300) # 1500 chars of one unit
|
|
31
|
+
with pytest.raises(RepetitionDetected):
|
|
32
|
+
list(_complete_nonstreaming(None, {}, stall_secs=300))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_varied_reasoning_does_not_raise(monkeypatch):
|
|
36
|
+
_patch(monkeypatch, "First check the file, then run grep, then write the result and verify it.")
|
|
37
|
+
out = list(_complete_nonstreaming(None, {}, stall_secs=300))
|
|
38
|
+
assert any(isinstance(o, AssistantTurn) for o in out) # completed normally
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_loop_but_has_toolcall_does_not_raise(monkeypatch):
|
|
42
|
+
_patch(monkeypatch, "loop " * 300, [_TC("Bash", '{"command":"ls"}')])
|
|
43
|
+
out = list(_complete_nonstreaming(None, {}, stall_secs=300))
|
|
44
|
+
turn = [o for o in out if isinstance(o, AssistantTurn)][0]
|
|
45
|
+
assert turn.tool_calls and turn.tool_calls[0]["name"] == "Bash" # action preserved
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_disabled_when_stall_secs_zero(monkeypatch):
|
|
49
|
+
_patch(monkeypatch, "loop " * 300)
|
|
50
|
+
out = list(_complete_nonstreaming(None, {}, stall_secs=0)) # feature off
|
|
51
|
+
assert any(isinstance(o, AssistantTurn) for o in out)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_agent_goes_decisive_immediately_on_repetition(monkeypatch):
|
|
55
|
+
calls = []
|
|
56
|
+
def fake_stream(model, system, messages, tool_schemas, config):
|
|
57
|
+
calls.append({"system": system, "max_tokens": config.get("max_tokens")})
|
|
58
|
+
if len(calls) == 1:
|
|
59
|
+
raise RepetitionDetected(0)
|
|
60
|
+
yield AssistantTurn("done", [], 10, 5)
|
|
61
|
+
monkeypatch.setattr(agent, "stream", fake_stream)
|
|
62
|
+
st = agent.AgentState()
|
|
63
|
+
cfg = {"model": "gemma4", "max_tokens": 8192, "stall_retry_secs": 300, "context_limit": 65536}
|
|
64
|
+
list(agent.run("go", st, cfg, "SYS"))
|
|
65
|
+
# first call raised repetition → SECOND call is already decisive (no plain retry)
|
|
66
|
+
assert len(calls) == 2
|
|
67
|
+
assert agent._DECISIVE_SUFFIX in calls[1]["system"]
|
|
68
|
+
assert calls[1]["max_tokens"] <= agent._DECISIVE_MAX_TOKENS
|
|
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
|
|
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
|