nanocode-cli 0.2.1__tar.gz → 0.2.3__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.
- {nanocode_cli-0.2.1/nanocode_cli.egg-info → nanocode_cli-0.2.3}/PKG-INFO +1 -1
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/nanocode.py +204 -65
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/pyproject.toml +1 -1
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/LICENSE +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/MANIFEST.in +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/README.md +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.3}/setup.cfg +0 -0
|
@@ -41,7 +41,7 @@ from prompt_toolkit.patch_stdout import patch_stdout
|
|
|
41
41
|
|
|
42
42
|
JsonValue: TypeAlias = Any
|
|
43
43
|
Json: TypeAlias = dict[str, JsonValue]
|
|
44
|
-
__version__ = "0.2.
|
|
44
|
+
__version__ = "0.2.3"
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
class Error(Exception): ...
|
|
@@ -524,6 +524,7 @@ class ToolCallExecution:
|
|
|
524
524
|
output: str
|
|
525
525
|
result_file: str
|
|
526
526
|
result_file_lines: int
|
|
527
|
+
log_written: bool = True
|
|
527
528
|
|
|
528
529
|
|
|
529
530
|
ConfirmationResult: TypeAlias = bool | str
|
|
@@ -565,6 +566,8 @@ def _range_fingerprint(content: str) -> str:
|
|
|
565
566
|
@final
|
|
566
567
|
@dataclass
|
|
567
568
|
class ReadTool(Tool):
|
|
569
|
+
MAX_LINES: ClassVar[int] = 1000
|
|
570
|
+
|
|
568
571
|
filepath: str = ""
|
|
569
572
|
start: int = 0
|
|
570
573
|
end: int = 0
|
|
@@ -580,7 +583,8 @@ class ReadTool(Tool):
|
|
|
580
583
|
return [
|
|
581
584
|
"Read exact file lines with a fingerprint.",
|
|
582
585
|
"Optional range is 0-based [start,end); end=0 means EOF.",
|
|
583
|
-
"
|
|
586
|
+
"Returns at most 1000 lines; truncated results include total lines and next-step guidance.",
|
|
587
|
+
"Prefer Search before Read for large or unknown files; use bounded reads when exact context is needed.",
|
|
584
588
|
"For ReplaceRange, call Read with the exact same filepath/start/end and reuse that range fingerprint.",
|
|
585
589
|
]
|
|
586
590
|
|
|
@@ -607,26 +611,64 @@ class ReadTool(Tool):
|
|
|
607
611
|
return f"Read({self.filepath}, {self.start}, {self.end})"
|
|
608
612
|
|
|
609
613
|
def call(self) -> str:
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
614
|
+
total_lines = 0
|
|
615
|
+
selected_lines = []
|
|
616
|
+
truncated = False
|
|
617
|
+
bounded_read_lines = self.end - self.start if self.end else 0
|
|
618
|
+
if self.end and bounded_read_lines <= self.MAX_LINES:
|
|
619
|
+
with open(self.filepath, "r", encoding="utf-8") as f:
|
|
620
|
+
selected_lines = list(itertools.islice(f, self.start, self.end))
|
|
621
|
+
else:
|
|
622
|
+
with open(self.filepath, "r", encoding="utf-8") as f:
|
|
623
|
+
for index, line in enumerate(f):
|
|
624
|
+
total_lines = index + 1
|
|
625
|
+
if index < self.start:
|
|
626
|
+
continue
|
|
627
|
+
if self.end and index >= self.end:
|
|
628
|
+
continue
|
|
629
|
+
if len(selected_lines) < self.MAX_LINES:
|
|
630
|
+
selected_lines.append(line)
|
|
631
|
+
continue
|
|
632
|
+
truncated = True
|
|
633
|
+
content = "".join(selected_lines)
|
|
634
|
+
returned_end = self.start + len(selected_lines)
|
|
635
|
+
fingerprint_end = returned_end if truncated else self.end
|
|
613
636
|
fingerprint = self.range_fingerprints.remember(
|
|
614
637
|
filepath=self.filepath,
|
|
615
638
|
start=self.start,
|
|
616
|
-
end=
|
|
639
|
+
end=fingerprint_end,
|
|
617
640
|
content=content,
|
|
618
641
|
)
|
|
619
|
-
|
|
642
|
+
lines = [
|
|
643
|
+
"<ReadToolResult>",
|
|
644
|
+
" <range>" + str(self.start) + ":" + str(fingerprint_end) + "</range>",
|
|
645
|
+
" <fingerprint>" + fingerprint + "</fingerprint>",
|
|
646
|
+
]
|
|
647
|
+
if truncated:
|
|
648
|
+
lines.extend(
|
|
649
|
+
[
|
|
650
|
+
" <truncated>true</truncated>",
|
|
651
|
+
" <total_lines>" + str(total_lines) + "</total_lines>",
|
|
652
|
+
" <note>Read returned "
|
|
653
|
+
+ str(len(selected_lines))
|
|
654
|
+
+ " lines from "
|
|
655
|
+
+ str(self.start)
|
|
656
|
+
+ ":"
|
|
657
|
+
+ str(returned_end)
|
|
658
|
+
+ " of "
|
|
659
|
+
+ str(total_lines)
|
|
660
|
+
+ " total lines. Use Search to locate relevant text or Read smaller ranges in batches.</note>",
|
|
661
|
+
]
|
|
662
|
+
)
|
|
663
|
+
lines.extend(
|
|
620
664
|
[
|
|
621
|
-
"<ReadToolResult>",
|
|
622
|
-
" <range>" + str(self.start) + ":" + str(self.end) + "</range>",
|
|
623
|
-
" <fingerprint>" + fingerprint + "</fingerprint>",
|
|
624
665
|
" <content no-indention>",
|
|
625
666
|
content,
|
|
626
667
|
" </content>",
|
|
627
668
|
"</ReadToolResult>",
|
|
628
669
|
]
|
|
629
670
|
)
|
|
671
|
+
return "\n".join(lines)
|
|
630
672
|
|
|
631
673
|
|
|
632
674
|
@final
|
|
@@ -687,18 +729,19 @@ class ListDirTool(Tool):
|
|
|
687
729
|
|
|
688
730
|
@classmethod
|
|
689
731
|
def signature(cls) -> str:
|
|
690
|
-
return
|
|
732
|
+
return 'ListDir(dir_path?: "."[, glob_pattern]) -> ListDirToolResult<entries>'
|
|
691
733
|
|
|
692
734
|
@classmethod
|
|
693
735
|
def example(cls) -> list[str]:
|
|
694
|
-
return ['Example args: ["."]', 'Example args: ["src", "*.py"]']
|
|
736
|
+
return ['Example args: []', 'Example args: ["."]', 'Example args: ["src", "*.py"]']
|
|
695
737
|
|
|
696
738
|
@classmethod
|
|
697
739
|
def make(cls, session: Session, args: list[str]) -> Self:
|
|
698
|
-
if len(args) not in (1, 2):
|
|
699
|
-
raise ToolCallError("requires
|
|
740
|
+
if len(args) not in (0, 1, 2):
|
|
741
|
+
raise ToolCallError("requires 0 to 2 args: [dir_path][, glob_pattern]")
|
|
742
|
+
dir_path = str(args[0]) if args else "."
|
|
700
743
|
glob_pattern = str(args[1]) if len(args) == 2 else ""
|
|
701
|
-
return cls(dirpath=session.resolve_path(
|
|
744
|
+
return cls(dirpath=session.resolve_path(dir_path), glob_pattern=glob_pattern, cwd=session.cwd)
|
|
702
745
|
|
|
703
746
|
def display(self) -> str:
|
|
704
747
|
if self.glob_pattern:
|
|
@@ -1664,8 +1707,11 @@ Tools:
|
|
|
1664
1707
|
- Call tools only by emitting JSON in tool_calls. Do not use native tool calls.
|
|
1665
1708
|
- Use multiple tool calls in one turn when they are independent.
|
|
1666
1709
|
- Prefer specific tools first; use Bash only when no provided tool fits.
|
|
1667
|
-
-
|
|
1668
|
-
-
|
|
1710
|
+
- Prefer Search before Read when locating code or facts; Read only known small ranges or exact files needed for editing.
|
|
1711
|
+
- Read returns at most 1000 lines; if truncated, use Search or smaller Read ranges in batches.
|
|
1712
|
+
- Summarize every latest tool result in last_tool_calls_summaries; raw results are shown once only, so include key_evidence when paths, lines, errors, or decisions matter later.
|
|
1713
|
+
- Latest tool results are already shown in Latest_Tool_Call_Results; use result_file logs only as a fallback when needed.
|
|
1714
|
+
- If an older tool result lacks detail that is needed for the task, prefer re-running a targeted source tool; Read result_file logs only when that is the cheapest accurate source.
|
|
1669
1715
|
- known_append is stable memory; current_context_update is task-local memory.
|
|
1670
1716
|
- tool_call.intention must state the question to answer, not just the action.
|
|
1671
1717
|
|
|
@@ -1949,6 +1995,8 @@ class ModelClient:
|
|
|
1949
1995
|
|
|
1950
1996
|
self._record_usage(_json_dict(result.get("usage") if isinstance(result, dict) else None))
|
|
1951
1997
|
content = self._message_content(result)
|
|
1998
|
+
if content is None:
|
|
1999
|
+
return self._invalid_model_response(self._format_missing_message_content(result))
|
|
1952
2000
|
return self._parse_model_content(content)
|
|
1953
2001
|
|
|
1954
2002
|
def _write_debug_prompt(self, *, activity: str, messages: list[Json]) -> str:
|
|
@@ -1978,13 +2026,9 @@ class ModelClient:
|
|
|
1978
2026
|
|
|
1979
2027
|
def _parse_model_content(self, content: str) -> Json:
|
|
1980
2028
|
text = content.strip()
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
lines = lines[1:]
|
|
1985
|
-
if lines and lines[-1].strip() == "```":
|
|
1986
|
-
lines = lines[:-1]
|
|
1987
|
-
text = "\n".join(lines).strip()
|
|
2029
|
+
text = self._strip_leaked_think_tags(text)
|
|
2030
|
+
text = self._strip_json_fence(text)
|
|
2031
|
+
text = self._strip_leaked_think_tags(text)
|
|
1988
2032
|
try:
|
|
1989
2033
|
value = json.loads(text)
|
|
1990
2034
|
except json.JSONDecodeError:
|
|
@@ -1993,15 +2037,49 @@ class ModelClient:
|
|
|
1993
2037
|
return self._invalid_model_response(content)
|
|
1994
2038
|
return value
|
|
1995
2039
|
|
|
2040
|
+
def _strip_json_fence(self, text: str) -> str:
|
|
2041
|
+
if not text.startswith("```"):
|
|
2042
|
+
return text
|
|
2043
|
+
lines = text.splitlines()
|
|
2044
|
+
if lines and lines[0].startswith("```"):
|
|
2045
|
+
lines = lines[1:]
|
|
2046
|
+
if lines and lines[-1].strip() == "```":
|
|
2047
|
+
lines = lines[:-1]
|
|
2048
|
+
return "\n".join(lines).strip()
|
|
2049
|
+
|
|
2050
|
+
def _strip_leaked_think_tags(self, text: str) -> str:
|
|
2051
|
+
text = text.strip()
|
|
2052
|
+
while text.startswith("</think>"):
|
|
2053
|
+
text = text[len("</think>") :].lstrip()
|
|
2054
|
+
while text.startswith("<think>"):
|
|
2055
|
+
end = text.find("</think>")
|
|
2056
|
+
if end < 0:
|
|
2057
|
+
return text
|
|
2058
|
+
text = text[end + len("</think>") :].lstrip()
|
|
2059
|
+
while text.startswith("</think>"):
|
|
2060
|
+
text = text[len("</think>") :].lstrip()
|
|
2061
|
+
return text
|
|
2062
|
+
|
|
1996
2063
|
def _invalid_model_response(self, content: str) -> Json:
|
|
2064
|
+
guidance = ""
|
|
2065
|
+
if self._looks_like_native_tool_call(content):
|
|
2066
|
+
guidance = (
|
|
2067
|
+
" Native tool_call syntax is not supported; return one JSON object with tool_calls entries like "
|
|
2068
|
+
'{"name":"Read","intention":"...","args":["nanocode.py","0","100"]}.'
|
|
2069
|
+
)
|
|
1997
2070
|
return {
|
|
1998
2071
|
"goal_reached": False,
|
|
1999
2072
|
"tool_calls": None,
|
|
2000
2073
|
"message_to_user": None,
|
|
2001
2074
|
"_format_error": "Invalid model output: expected one JSON object matching the Output JSON schema. Return strict JSON only. Bad output: "
|
|
2002
|
-
+ _shorten(content)
|
|
2075
|
+
+ _shorten(content)
|
|
2076
|
+
+ guidance,
|
|
2003
2077
|
}
|
|
2004
2078
|
|
|
2079
|
+
def _looks_like_native_tool_call(self, content: str) -> bool:
|
|
2080
|
+
text = self._strip_leaked_think_tags(content.strip())
|
|
2081
|
+
return text.startswith("<tool_call>")
|
|
2082
|
+
|
|
2005
2083
|
def _chat_completions_url(self) -> str:
|
|
2006
2084
|
url = self.session.api_url.rstrip("/")
|
|
2007
2085
|
if url.endswith("/chat/completions"):
|
|
@@ -2015,7 +2093,7 @@ class ModelClient:
|
|
|
2015
2093
|
return {"reasoning": {"effort": self.session.reasoning_effort}}
|
|
2016
2094
|
return {}
|
|
2017
2095
|
|
|
2018
|
-
def _message_content(self, result: JsonValue) -> str:
|
|
2096
|
+
def _message_content(self, result: JsonValue) -> str | None:
|
|
2019
2097
|
data = _json_dict(result)
|
|
2020
2098
|
choices = _json_list(data.get("choices"))
|
|
2021
2099
|
if not choices:
|
|
@@ -2023,9 +2101,18 @@ class ModelClient:
|
|
|
2023
2101
|
message = _json_dict(_json_dict(choices[0]).get("message"))
|
|
2024
2102
|
content = message.get("content")
|
|
2025
2103
|
if not isinstance(content, str):
|
|
2026
|
-
|
|
2104
|
+
return None
|
|
2027
2105
|
return content
|
|
2028
2106
|
|
|
2107
|
+
def _format_missing_message_content(self, result: JsonValue) -> str:
|
|
2108
|
+
choice = _json_dict(_json_list(_json_dict(result).get("choices"))[0])
|
|
2109
|
+
message = _json_dict(choice.get("message"))
|
|
2110
|
+
details: Json = {
|
|
2111
|
+
"finish_reason": choice.get("finish_reason"),
|
|
2112
|
+
"message_keys": sorted(str(key) for key in message.keys()),
|
|
2113
|
+
}
|
|
2114
|
+
return "API response missing message content: " + json.dumps(details, ensure_ascii=False)
|
|
2115
|
+
|
|
2029
2116
|
def _record_usage(self, usage: Json) -> None:
|
|
2030
2117
|
prompt_tokens = _json_int(usage.get("prompt_tokens"))
|
|
2031
2118
|
completion_tokens = _json_int(usage.get("completion_tokens"))
|
|
@@ -2092,25 +2179,32 @@ class ToolCallRunner:
|
|
|
2092
2179
|
if call is None:
|
|
2093
2180
|
call = self._invalid_tool_call(item)
|
|
2094
2181
|
|
|
2095
|
-
|
|
2182
|
+
log_written = not self._is_tool_result_file_read(call)
|
|
2183
|
+
if log_written:
|
|
2184
|
+
result_file, result_file_lines = self._write_tool_result_log(call, outcome, output)
|
|
2185
|
+
else:
|
|
2186
|
+
result_file = os.path.relpath(self.session.resolve_path(call.args[0]), self.session.cwd)
|
|
2187
|
+
result_file_lines = len(output.splitlines())
|
|
2096
2188
|
execution = ToolCallExecution(
|
|
2097
2189
|
call=call,
|
|
2098
2190
|
outcome=outcome,
|
|
2099
2191
|
output=output,
|
|
2100
2192
|
result_file=result_file,
|
|
2101
2193
|
result_file_lines=result_file_lines,
|
|
2194
|
+
log_written=log_written,
|
|
2102
2195
|
)
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2196
|
+
if log_written:
|
|
2197
|
+
event = ToolCallEvent(
|
|
2198
|
+
intent=call.intention,
|
|
2199
|
+
executed=call.executed,
|
|
2200
|
+
outcome=outcome,
|
|
2201
|
+
summary="",
|
|
2202
|
+
result_file=result_file,
|
|
2203
|
+
result_file_lines=result_file_lines,
|
|
2204
|
+
)
|
|
2205
|
+
self.session.append_conversation(event)
|
|
2206
|
+
events.append(event)
|
|
2112
2207
|
executions.append(execution)
|
|
2113
|
-
events.append(event)
|
|
2114
2208
|
|
|
2115
2209
|
self.latest_events = events
|
|
2116
2210
|
self.latest_executions = executions
|
|
@@ -2128,7 +2222,8 @@ class ToolCallRunner:
|
|
|
2128
2222
|
lines.append(" " + str(index) + ". [" + execution.outcome + "] " + execution.call.executed)
|
|
2129
2223
|
if execution.call.intention:
|
|
2130
2224
|
lines.append(" why: " + execution.call.intention)
|
|
2131
|
-
|
|
2225
|
+
label = "log" if execution.log_written else "source"
|
|
2226
|
+
lines.append(" " + label + ": " + execution.result_file + " (" + str(execution.result_file_lines) + " lines)")
|
|
2132
2227
|
return "\n".join(lines)
|
|
2133
2228
|
|
|
2134
2229
|
def parse_tool_call(self, value: JsonValue) -> ParsedToolCall:
|
|
@@ -2153,6 +2248,16 @@ class ToolCallRunner:
|
|
|
2153
2248
|
raise ToolCallError("tool not found: " + call.name)
|
|
2154
2249
|
return tool_class.make(self.session, call.args)
|
|
2155
2250
|
|
|
2251
|
+
def _is_tool_result_file_read(self, call: ParsedToolCall) -> bool:
|
|
2252
|
+
if call.name != ReadTool.name() or not call.args:
|
|
2253
|
+
return False
|
|
2254
|
+
tool_results_dir = os.path.realpath(self.session.tool_results_dir())
|
|
2255
|
+
path = os.path.realpath(self.session.resolve_path(call.args[0]))
|
|
2256
|
+
try:
|
|
2257
|
+
return os.path.commonpath([tool_results_dir, path]) == tool_results_dir
|
|
2258
|
+
except ValueError:
|
|
2259
|
+
return False
|
|
2260
|
+
|
|
2156
2261
|
def _write_tool_result_log(self, call: ParsedToolCall, outcome: str, output: str) -> tuple[str, int]:
|
|
2157
2262
|
directory = self.session.tool_results_dir()
|
|
2158
2263
|
os.makedirs(directory, exist_ok=True)
|
|
@@ -2563,8 +2668,7 @@ class ConversationCompactor:
|
|
|
2563
2668
|
|
|
2564
2669
|
@final
|
|
2565
2670
|
class Agent:
|
|
2566
|
-
|
|
2567
|
-
EVIDENCE_OUTPUT_CHARS: ClassVar[int] = 4000
|
|
2671
|
+
MAX_CONSECUTIVE_FORMAT_ERRORS: ClassVar[int] = 3
|
|
2568
2672
|
|
|
2569
2673
|
def __init__(self, session: Session):
|
|
2570
2674
|
self.session = session
|
|
@@ -2610,18 +2714,48 @@ class Agent:
|
|
|
2610
2714
|
self.session.current.goal_reached = False
|
|
2611
2715
|
self.maybe_auto_compact()
|
|
2612
2716
|
self.session.append_conversation(UserMessage(content=user_input))
|
|
2717
|
+
consecutive_format_errors = 0
|
|
2613
2718
|
|
|
2614
2719
|
for _ in range(self.session.max_agent_steps):
|
|
2615
2720
|
response = self.step()
|
|
2616
2721
|
format_error = _json_str(response.get("_format_error"))
|
|
2617
2722
|
if format_error:
|
|
2723
|
+
consecutive_format_errors += 1
|
|
2618
2724
|
self.latest_tool_call_results = format_error
|
|
2725
|
+
if consecutive_format_errors >= self.MAX_CONSECUTIVE_FORMAT_ERRORS:
|
|
2726
|
+
self._report_gate(
|
|
2727
|
+
on_message,
|
|
2728
|
+
"Stopped: model returned invalid output "
|
|
2729
|
+
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2730
|
+
+ " times in a row.",
|
|
2731
|
+
"Format_Gate: stopped after "
|
|
2732
|
+
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2733
|
+
+ " consecutive invalid model outputs. "
|
|
2734
|
+
+ _shorten(format_error, 180),
|
|
2735
|
+
)
|
|
2736
|
+
raise LLMError(
|
|
2737
|
+
"model returned invalid output "
|
|
2738
|
+
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2739
|
+
+ " times in a row: "
|
|
2740
|
+
+ _shorten(format_error, 300)
|
|
2741
|
+
)
|
|
2742
|
+
self._report_gate(
|
|
2743
|
+
on_message,
|
|
2744
|
+
"Retrying: model returned invalid output.",
|
|
2745
|
+
"Format_Gate: retrying model response. " + _shorten(format_error, 180),
|
|
2746
|
+
)
|
|
2619
2747
|
continue
|
|
2748
|
+
consecutive_format_errors = 0
|
|
2620
2749
|
tool_calls = _json_list(response.get("tool_calls"))
|
|
2621
2750
|
summary_gate = self._format_tool_summary_gate(tool_calls)
|
|
2622
2751
|
if summary_gate:
|
|
2623
2752
|
self.state_updater.latest_report = ""
|
|
2624
2753
|
self.latest_tool_call_results = summary_gate
|
|
2754
|
+
self._report_gate(
|
|
2755
|
+
on_message,
|
|
2756
|
+
"Retrying: model needs to summarize the latest tool results.",
|
|
2757
|
+
self._compact_gate_report(summary_gate),
|
|
2758
|
+
)
|
|
2625
2759
|
continue
|
|
2626
2760
|
self.apply_response(response)
|
|
2627
2761
|
if on_message is not None and self.state_updater.latest_report:
|
|
@@ -2644,13 +2778,35 @@ class Agent:
|
|
|
2644
2778
|
if self.session.current.verification.status == VerificationStatus.REQUIRED:
|
|
2645
2779
|
self.session.current.goal_reached = False
|
|
2646
2780
|
self.latest_tool_call_results = self._format_verification_gate()
|
|
2781
|
+
self._report_gate(
|
|
2782
|
+
on_message,
|
|
2783
|
+
"Retrying: verification is required before completion.",
|
|
2784
|
+
"Verification_Gate: retrying until verification is passed or blocked.",
|
|
2785
|
+
)
|
|
2647
2786
|
continue
|
|
2648
2787
|
if not self.session.current.goal_reached:
|
|
2649
2788
|
self.latest_tool_call_results = self._format_continuation_hint()
|
|
2789
|
+
self._report_gate(
|
|
2790
|
+
on_message,
|
|
2791
|
+
"Continuing: goal is not complete yet.",
|
|
2792
|
+
"Continuation_Gate: goal not reached; retrying next useful action.",
|
|
2793
|
+
)
|
|
2650
2794
|
continue
|
|
2651
2795
|
return response
|
|
2652
2796
|
raise LLMError("agent step limit reached")
|
|
2653
2797
|
|
|
2798
|
+
def _report_gate(self, on_message: MessageCallback | None, message: str, debug_message: str) -> None:
|
|
2799
|
+
if on_message is not None:
|
|
2800
|
+
on_message(debug_message if self.session.debug else message)
|
|
2801
|
+
|
|
2802
|
+
def _compact_gate_report(self, gate: str) -> str:
|
|
2803
|
+
lines = gate.splitlines()
|
|
2804
|
+
headline = lines[0] if lines else "Gate"
|
|
2805
|
+
details = [line for line in lines[1:] if line.startswith("- ")]
|
|
2806
|
+
if details:
|
|
2807
|
+
return headline + ": " + _shorten("; ".join(details[:3]), 220)
|
|
2808
|
+
return headline
|
|
2809
|
+
|
|
2654
2810
|
def step(self) -> Json:
|
|
2655
2811
|
response = self.request(self.build_system_prompt(), self.build_user_prompt(), activity="main")
|
|
2656
2812
|
self.state_updater.apply_tool_call_summaries(response)
|
|
@@ -2688,49 +2844,32 @@ class Agent:
|
|
|
2688
2844
|
|
|
2689
2845
|
def _format_tool_summary_gate(self, tool_calls: list[JsonValue]) -> str:
|
|
2690
2846
|
missing = self._missing_tool_summaries()
|
|
2691
|
-
missing_evidence = []
|
|
2692
2847
|
needs_read = []
|
|
2693
|
-
for event
|
|
2848
|
+
for event in self.tool_runner.latest_events:
|
|
2694
2849
|
if not event.summary:
|
|
2695
2850
|
continue
|
|
2696
2851
|
is_reading_result_file = self._has_read_result_file_call(tool_calls, event.result_file)
|
|
2697
|
-
if event.needs_raw_read
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
if not event.key_details and not is_reading_result_file:
|
|
2701
|
-
missing_evidence.append(event)
|
|
2852
|
+
if event.needs_raw_read:
|
|
2853
|
+
if not is_reading_result_file:
|
|
2854
|
+
needs_read.append(event)
|
|
2702
2855
|
continue
|
|
2703
|
-
|
|
2704
|
-
self._is_large_tool_output(execution.output)
|
|
2705
|
-
and not event.key_details
|
|
2706
|
-
and not event.needs_raw_read
|
|
2707
|
-
and not is_reading_result_file
|
|
2708
|
-
):
|
|
2709
|
-
missing_evidence.append(event)
|
|
2710
|
-
if not missing and not missing_evidence and not needs_read:
|
|
2856
|
+
if not missing and not needs_read:
|
|
2711
2857
|
return ""
|
|
2712
2858
|
|
|
2713
|
-
lines = ["Tool_Summary_Gate:
|
|
2859
|
+
lines = ["Tool_Summary_Gate: summarize latest tool results before continuing.", "Raw tool results are visible only once."]
|
|
2714
2860
|
if missing:
|
|
2715
2861
|
lines.append("Missing summaries:")
|
|
2716
2862
|
for event in missing:
|
|
2717
2863
|
lines.append("- " + event.executed + " -> " + event.result_file)
|
|
2718
|
-
if missing_evidence:
|
|
2719
|
-
lines.append("Missing key_evidence:")
|
|
2720
|
-
for event in missing_evidence:
|
|
2721
|
-
lines.append("- " + event.executed + " -> " + event.result_file)
|
|
2722
2864
|
if needs_read:
|
|
2723
2865
|
lines.append("Needs raw read:")
|
|
2724
2866
|
for event in needs_read:
|
|
2725
2867
|
lines.append("- Read(" + event.result_file + ") before continuing")
|
|
2726
2868
|
lines.append(
|
|
2727
|
-
"Next_Action: return last_tool_calls_summaries
|
|
2869
|
+
"Next_Action: return last_tool_calls_summaries for missing results, read result_file only when it is the cheapest accurate fallback, or continue with source tools such as Search/ListDir."
|
|
2728
2870
|
)
|
|
2729
2871
|
return "\n".join(lines)
|
|
2730
2872
|
|
|
2731
|
-
def _is_large_tool_output(self, output: str) -> bool:
|
|
2732
|
-
return len(output) > self.EVIDENCE_OUTPUT_CHARS or len(output.splitlines()) > self.EVIDENCE_OUTPUT_LINES
|
|
2733
|
-
|
|
2734
2873
|
def _has_read_result_file_call(self, tool_calls: list[JsonValue], result_file: str) -> bool:
|
|
2735
2874
|
if not result_file:
|
|
2736
2875
|
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|