nanocode-cli 0.2.2__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.2/nanocode_cli.egg-info → nanocode_cli-0.2.3}/PKG-INFO +1 -1
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3}/nanocode.py +106 -11
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3}/pyproject.toml +1 -1
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3}/LICENSE +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3}/MANIFEST.in +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3}/README.md +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.3}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.2.2 → 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): ...
|
|
@@ -1995,6 +1995,8 @@ class ModelClient:
|
|
|
1995
1995
|
|
|
1996
1996
|
self._record_usage(_json_dict(result.get("usage") if isinstance(result, dict) else None))
|
|
1997
1997
|
content = self._message_content(result)
|
|
1998
|
+
if content is None:
|
|
1999
|
+
return self._invalid_model_response(self._format_missing_message_content(result))
|
|
1998
2000
|
return self._parse_model_content(content)
|
|
1999
2001
|
|
|
2000
2002
|
def _write_debug_prompt(self, *, activity: str, messages: list[Json]) -> str:
|
|
@@ -2024,13 +2026,9 @@ class ModelClient:
|
|
|
2024
2026
|
|
|
2025
2027
|
def _parse_model_content(self, content: str) -> Json:
|
|
2026
2028
|
text = content.strip()
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
lines = lines[1:]
|
|
2031
|
-
if lines and lines[-1].strip() == "```":
|
|
2032
|
-
lines = lines[:-1]
|
|
2033
|
-
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)
|
|
2034
2032
|
try:
|
|
2035
2033
|
value = json.loads(text)
|
|
2036
2034
|
except json.JSONDecodeError:
|
|
@@ -2039,15 +2037,49 @@ class ModelClient:
|
|
|
2039
2037
|
return self._invalid_model_response(content)
|
|
2040
2038
|
return value
|
|
2041
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
|
+
|
|
2042
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
|
+
)
|
|
2043
2070
|
return {
|
|
2044
2071
|
"goal_reached": False,
|
|
2045
2072
|
"tool_calls": None,
|
|
2046
2073
|
"message_to_user": None,
|
|
2047
2074
|
"_format_error": "Invalid model output: expected one JSON object matching the Output JSON schema. Return strict JSON only. Bad output: "
|
|
2048
|
-
+ _shorten(content)
|
|
2075
|
+
+ _shorten(content)
|
|
2076
|
+
+ guidance,
|
|
2049
2077
|
}
|
|
2050
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
|
+
|
|
2051
2083
|
def _chat_completions_url(self) -> str:
|
|
2052
2084
|
url = self.session.api_url.rstrip("/")
|
|
2053
2085
|
if url.endswith("/chat/completions"):
|
|
@@ -2061,7 +2093,7 @@ class ModelClient:
|
|
|
2061
2093
|
return {"reasoning": {"effort": self.session.reasoning_effort}}
|
|
2062
2094
|
return {}
|
|
2063
2095
|
|
|
2064
|
-
def _message_content(self, result: JsonValue) -> str:
|
|
2096
|
+
def _message_content(self, result: JsonValue) -> str | None:
|
|
2065
2097
|
data = _json_dict(result)
|
|
2066
2098
|
choices = _json_list(data.get("choices"))
|
|
2067
2099
|
if not choices:
|
|
@@ -2069,9 +2101,18 @@ class ModelClient:
|
|
|
2069
2101
|
message = _json_dict(_json_dict(choices[0]).get("message"))
|
|
2070
2102
|
content = message.get("content")
|
|
2071
2103
|
if not isinstance(content, str):
|
|
2072
|
-
|
|
2104
|
+
return None
|
|
2073
2105
|
return content
|
|
2074
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
|
+
|
|
2075
2116
|
def _record_usage(self, usage: Json) -> None:
|
|
2076
2117
|
prompt_tokens = _json_int(usage.get("prompt_tokens"))
|
|
2077
2118
|
completion_tokens = _json_int(usage.get("completion_tokens"))
|
|
@@ -2627,6 +2668,8 @@ class ConversationCompactor:
|
|
|
2627
2668
|
|
|
2628
2669
|
@final
|
|
2629
2670
|
class Agent:
|
|
2671
|
+
MAX_CONSECUTIVE_FORMAT_ERRORS: ClassVar[int] = 3
|
|
2672
|
+
|
|
2630
2673
|
def __init__(self, session: Session):
|
|
2631
2674
|
self.session = session
|
|
2632
2675
|
self.prompt_builder = PromptBuilder(session)
|
|
@@ -2671,18 +2714,48 @@ class Agent:
|
|
|
2671
2714
|
self.session.current.goal_reached = False
|
|
2672
2715
|
self.maybe_auto_compact()
|
|
2673
2716
|
self.session.append_conversation(UserMessage(content=user_input))
|
|
2717
|
+
consecutive_format_errors = 0
|
|
2674
2718
|
|
|
2675
2719
|
for _ in range(self.session.max_agent_steps):
|
|
2676
2720
|
response = self.step()
|
|
2677
2721
|
format_error = _json_str(response.get("_format_error"))
|
|
2678
2722
|
if format_error:
|
|
2723
|
+
consecutive_format_errors += 1
|
|
2679
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
|
+
)
|
|
2680
2747
|
continue
|
|
2748
|
+
consecutive_format_errors = 0
|
|
2681
2749
|
tool_calls = _json_list(response.get("tool_calls"))
|
|
2682
2750
|
summary_gate = self._format_tool_summary_gate(tool_calls)
|
|
2683
2751
|
if summary_gate:
|
|
2684
2752
|
self.state_updater.latest_report = ""
|
|
2685
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
|
+
)
|
|
2686
2759
|
continue
|
|
2687
2760
|
self.apply_response(response)
|
|
2688
2761
|
if on_message is not None and self.state_updater.latest_report:
|
|
@@ -2705,13 +2778,35 @@ class Agent:
|
|
|
2705
2778
|
if self.session.current.verification.status == VerificationStatus.REQUIRED:
|
|
2706
2779
|
self.session.current.goal_reached = False
|
|
2707
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
|
+
)
|
|
2708
2786
|
continue
|
|
2709
2787
|
if not self.session.current.goal_reached:
|
|
2710
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
|
+
)
|
|
2711
2794
|
continue
|
|
2712
2795
|
return response
|
|
2713
2796
|
raise LLMError("agent step limit reached")
|
|
2714
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
|
+
|
|
2715
2810
|
def step(self) -> Json:
|
|
2716
2811
|
response = self.request(self.build_system_prompt(), self.build_user_prompt(), activity="main")
|
|
2717
2812
|
self.state_updater.apply_tool_call_summaries(response)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|