nanocode-cli 0.2.2__tar.gz → 0.2.4__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.4}/PKG-INFO +1 -1
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/nanocode.py +135 -20
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/pyproject.toml +1 -1
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/LICENSE +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/MANIFEST.in +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/README.md +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.2.2 → nanocode_cli-0.2.4}/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.4"
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
class Error(Exception): ...
|
|
@@ -1704,7 +1704,7 @@ Core:
|
|
|
1704
1704
|
- Follow the plan, but revise it when facts require it.
|
|
1705
1705
|
|
|
1706
1706
|
Tools:
|
|
1707
|
-
-
|
|
1707
|
+
- MUST use JSON tool_calls. Do not use native <tool_call>Tool(args...) syntax.
|
|
1708
1708
|
- Use multiple tool calls in one turn when they are independent.
|
|
1709
1709
|
- Prefer specific tools first; use Bash only when no provided tool fits.
|
|
1710
1710
|
- Prefer Search before Read when locating code or facts; Read only known small ranges or exact files needed for editing.
|
|
@@ -1729,6 +1729,7 @@ Input:
|
|
|
1729
1729
|
- Current_Context: task-local facts that may expire
|
|
1730
1730
|
- Goal: current objective
|
|
1731
1731
|
- Plan: ordered plan
|
|
1732
|
+
- Agent_Feedback: latest retry/gate warning for you; follow it before continuing
|
|
1732
1733
|
- Latest_Tool_Call_Results: latest raw tool call results
|
|
1733
1734
|
- Latest_User_Input: latest user message
|
|
1734
1735
|
- Tools: available tool specs
|
|
@@ -1834,6 +1835,10 @@ MAIN_AGENT_USER_PROMPT_TEMPLATE = """
|
|
|
1834
1835
|
{verification_state}
|
|
1835
1836
|
-------- Verification_State End -----------
|
|
1836
1837
|
|
|
1838
|
+
----------- Agent_Feedback Begin ------
|
|
1839
|
+
{agent_feedback}
|
|
1840
|
+
-------- Agent_Feedback End -----------
|
|
1841
|
+
|
|
1837
1842
|
----------- Latest_Tool_Call_Results Begin ------
|
|
1838
1843
|
{latest_tool_call_results}
|
|
1839
1844
|
-------- Latest_Tool_Call_Results End -----------
|
|
@@ -1888,7 +1893,7 @@ class PromptBuilder:
|
|
|
1888
1893
|
def system_prompt(self) -> str:
|
|
1889
1894
|
return MAIN_AGENT_SYSTEM_PROMPT.replace("{ __tools__ }", self._format_tools()).strip()
|
|
1890
1895
|
|
|
1891
|
-
def user_prompt(self, latest_tool_call_results: str) -> str:
|
|
1896
|
+
def user_prompt(self, latest_tool_call_results: str, agent_feedback: str) -> str:
|
|
1892
1897
|
current = self.session.current
|
|
1893
1898
|
return MAIN_AGENT_USER_PROMPT_TEMPLATE.format(
|
|
1894
1899
|
environment=self._format_environment(),
|
|
@@ -1898,6 +1903,7 @@ class PromptBuilder:
|
|
|
1898
1903
|
goal=current.goal or "(empty)",
|
|
1899
1904
|
plan=self._format_plan(),
|
|
1900
1905
|
verification_state=current.verification.format(),
|
|
1906
|
+
agent_feedback=agent_feedback or "(empty)",
|
|
1901
1907
|
latest_tool_call_results=latest_tool_call_results or "(empty)",
|
|
1902
1908
|
latest_user_input=current.user_input or "(empty)",
|
|
1903
1909
|
).strip()
|
|
@@ -1995,6 +2001,8 @@ class ModelClient:
|
|
|
1995
2001
|
|
|
1996
2002
|
self._record_usage(_json_dict(result.get("usage") if isinstance(result, dict) else None))
|
|
1997
2003
|
content = self._message_content(result)
|
|
2004
|
+
if content is None:
|
|
2005
|
+
return self._invalid_model_response(self._format_missing_message_content(result))
|
|
1998
2006
|
return self._parse_model_content(content)
|
|
1999
2007
|
|
|
2000
2008
|
def _write_debug_prompt(self, *, activity: str, messages: list[Json]) -> str:
|
|
@@ -2024,13 +2032,9 @@ class ModelClient:
|
|
|
2024
2032
|
|
|
2025
2033
|
def _parse_model_content(self, content: str) -> Json:
|
|
2026
2034
|
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()
|
|
2035
|
+
text = self._strip_leaked_think_tags(text)
|
|
2036
|
+
text = self._strip_json_fence(text)
|
|
2037
|
+
text = self._strip_leaked_think_tags(text)
|
|
2034
2038
|
try:
|
|
2035
2039
|
value = json.loads(text)
|
|
2036
2040
|
except json.JSONDecodeError:
|
|
@@ -2039,15 +2043,49 @@ class ModelClient:
|
|
|
2039
2043
|
return self._invalid_model_response(content)
|
|
2040
2044
|
return value
|
|
2041
2045
|
|
|
2046
|
+
def _strip_json_fence(self, text: str) -> str:
|
|
2047
|
+
if not text.startswith("```"):
|
|
2048
|
+
return text
|
|
2049
|
+
lines = text.splitlines()
|
|
2050
|
+
if lines and lines[0].startswith("```"):
|
|
2051
|
+
lines = lines[1:]
|
|
2052
|
+
if lines and lines[-1].strip() == "```":
|
|
2053
|
+
lines = lines[:-1]
|
|
2054
|
+
return "\n".join(lines).strip()
|
|
2055
|
+
|
|
2056
|
+
def _strip_leaked_think_tags(self, text: str) -> str:
|
|
2057
|
+
text = text.strip()
|
|
2058
|
+
while text.startswith("</think>"):
|
|
2059
|
+
text = text[len("</think>") :].lstrip()
|
|
2060
|
+
while text.startswith("<think>"):
|
|
2061
|
+
end = text.find("</think>")
|
|
2062
|
+
if end < 0:
|
|
2063
|
+
return text
|
|
2064
|
+
text = text[end + len("</think>") :].lstrip()
|
|
2065
|
+
while text.startswith("</think>"):
|
|
2066
|
+
text = text[len("</think>") :].lstrip()
|
|
2067
|
+
return text
|
|
2068
|
+
|
|
2042
2069
|
def _invalid_model_response(self, content: str) -> Json:
|
|
2070
|
+
guidance = ""
|
|
2071
|
+
if self._looks_like_native_tool_call(content):
|
|
2072
|
+
guidance = (
|
|
2073
|
+
" Native tool_call syntax is not supported; return one JSON object with tool_calls entries like "
|
|
2074
|
+
'{"name":"Read","intention":"...","args":["nanocode.py","0","100"]}.'
|
|
2075
|
+
)
|
|
2043
2076
|
return {
|
|
2044
2077
|
"goal_reached": False,
|
|
2045
2078
|
"tool_calls": None,
|
|
2046
2079
|
"message_to_user": None,
|
|
2047
2080
|
"_format_error": "Invalid model output: expected one JSON object matching the Output JSON schema. Return strict JSON only. Bad output: "
|
|
2048
|
-
+ _shorten(content)
|
|
2081
|
+
+ _shorten(content)
|
|
2082
|
+
+ guidance,
|
|
2049
2083
|
}
|
|
2050
2084
|
|
|
2085
|
+
def _looks_like_native_tool_call(self, content: str) -> bool:
|
|
2086
|
+
text = self._strip_leaked_think_tags(content.strip())
|
|
2087
|
+
return text.startswith("<tool_call>")
|
|
2088
|
+
|
|
2051
2089
|
def _chat_completions_url(self) -> str:
|
|
2052
2090
|
url = self.session.api_url.rstrip("/")
|
|
2053
2091
|
if url.endswith("/chat/completions"):
|
|
@@ -2061,7 +2099,7 @@ class ModelClient:
|
|
|
2061
2099
|
return {"reasoning": {"effort": self.session.reasoning_effort}}
|
|
2062
2100
|
return {}
|
|
2063
2101
|
|
|
2064
|
-
def _message_content(self, result: JsonValue) -> str:
|
|
2102
|
+
def _message_content(self, result: JsonValue) -> str | None:
|
|
2065
2103
|
data = _json_dict(result)
|
|
2066
2104
|
choices = _json_list(data.get("choices"))
|
|
2067
2105
|
if not choices:
|
|
@@ -2069,9 +2107,18 @@ class ModelClient:
|
|
|
2069
2107
|
message = _json_dict(_json_dict(choices[0]).get("message"))
|
|
2070
2108
|
content = message.get("content")
|
|
2071
2109
|
if not isinstance(content, str):
|
|
2072
|
-
|
|
2110
|
+
return None
|
|
2073
2111
|
return content
|
|
2074
2112
|
|
|
2113
|
+
def _format_missing_message_content(self, result: JsonValue) -> str:
|
|
2114
|
+
choice = _json_dict(_json_list(_json_dict(result).get("choices"))[0])
|
|
2115
|
+
message = _json_dict(choice.get("message"))
|
|
2116
|
+
details: Json = {
|
|
2117
|
+
"finish_reason": choice.get("finish_reason"),
|
|
2118
|
+
"message_keys": sorted(str(key) for key in message.keys()),
|
|
2119
|
+
}
|
|
2120
|
+
return "API response missing message content: " + json.dumps(details, ensure_ascii=False)
|
|
2121
|
+
|
|
2075
2122
|
def _record_usage(self, usage: Json) -> None:
|
|
2076
2123
|
prompt_tokens = _json_int(usage.get("prompt_tokens"))
|
|
2077
2124
|
completion_tokens = _json_int(usage.get("completion_tokens"))
|
|
@@ -2627,6 +2674,9 @@ class ConversationCompactor:
|
|
|
2627
2674
|
|
|
2628
2675
|
@final
|
|
2629
2676
|
class Agent:
|
|
2677
|
+
MAX_CONSECUTIVE_FORMAT_ERRORS: ClassVar[int] = 3
|
|
2678
|
+
MAX_CONSECUTIVE_SUMMARY_GATES: ClassVar[int] = 1
|
|
2679
|
+
|
|
2630
2680
|
def __init__(self, session: Session):
|
|
2631
2681
|
self.session = session
|
|
2632
2682
|
self.prompt_builder = PromptBuilder(session)
|
|
@@ -2635,6 +2685,7 @@ class Agent:
|
|
|
2635
2685
|
self.state_updater = AgentStateUpdater(session, self.tool_runner)
|
|
2636
2686
|
self.compactor = ConversationCompactor(session, self.model_client)
|
|
2637
2687
|
self.latest_tool_call_results = ""
|
|
2688
|
+
self.latest_agent_feedback = ""
|
|
2638
2689
|
|
|
2639
2690
|
@property
|
|
2640
2691
|
def latest_tool_call_events(self) -> list[ToolCallEvent]:
|
|
@@ -2644,9 +2695,10 @@ class Agent:
|
|
|
2644
2695
|
return self.prompt_builder.system_prompt()
|
|
2645
2696
|
|
|
2646
2697
|
def build_user_prompt(self, *, consume_latest_tool_results: bool = True) -> str:
|
|
2647
|
-
prompt = self.prompt_builder.user_prompt(self.latest_tool_call_results)
|
|
2698
|
+
prompt = self.prompt_builder.user_prompt(self.latest_tool_call_results, self.latest_agent_feedback)
|
|
2648
2699
|
if consume_latest_tool_results:
|
|
2649
2700
|
self.latest_tool_call_results = ""
|
|
2701
|
+
self.latest_agent_feedback = ""
|
|
2650
2702
|
return prompt
|
|
2651
2703
|
|
|
2652
2704
|
def request(self, system_prompt: str, user_prompt: str, *, activity: str = "main") -> Json:
|
|
@@ -2667,23 +2719,64 @@ class Agent:
|
|
|
2667
2719
|
on_message: MessageCallback | None = None,
|
|
2668
2720
|
) -> Json:
|
|
2669
2721
|
self.latest_tool_call_results = ""
|
|
2722
|
+
self.latest_agent_feedback = ""
|
|
2670
2723
|
self.session.current.user_input = user_input
|
|
2671
2724
|
self.session.current.goal_reached = False
|
|
2672
2725
|
self.maybe_auto_compact()
|
|
2673
2726
|
self.session.append_conversation(UserMessage(content=user_input))
|
|
2727
|
+
consecutive_format_errors = 0
|
|
2728
|
+
consecutive_summary_gates = 0
|
|
2674
2729
|
|
|
2675
2730
|
for _ in range(self.session.max_agent_steps):
|
|
2676
2731
|
response = self.step()
|
|
2677
2732
|
format_error = _json_str(response.get("_format_error"))
|
|
2678
2733
|
if format_error:
|
|
2679
|
-
|
|
2734
|
+
consecutive_format_errors += 1
|
|
2735
|
+
self.latest_agent_feedback = format_error
|
|
2736
|
+
if consecutive_format_errors >= self.MAX_CONSECUTIVE_FORMAT_ERRORS:
|
|
2737
|
+
self._report_gate(
|
|
2738
|
+
on_message,
|
|
2739
|
+
"Stopped: model returned invalid output "
|
|
2740
|
+
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2741
|
+
+ " times in a row.",
|
|
2742
|
+
"Format_Gate: stopped after "
|
|
2743
|
+
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2744
|
+
+ " consecutive invalid model outputs. "
|
|
2745
|
+
+ _shorten(format_error, 180),
|
|
2746
|
+
)
|
|
2747
|
+
raise LLMError(
|
|
2748
|
+
"model returned invalid output "
|
|
2749
|
+
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2750
|
+
+ " times in a row: "
|
|
2751
|
+
+ _shorten(format_error, 300)
|
|
2752
|
+
)
|
|
2753
|
+
self._report_gate(
|
|
2754
|
+
on_message,
|
|
2755
|
+
"Retrying: model returned invalid output.",
|
|
2756
|
+
"Format_Gate: retrying model response. " + _shorten(format_error, 180),
|
|
2757
|
+
)
|
|
2680
2758
|
continue
|
|
2759
|
+
consecutive_format_errors = 0
|
|
2681
2760
|
tool_calls = _json_list(response.get("tool_calls"))
|
|
2682
2761
|
summary_gate = self._format_tool_summary_gate(tool_calls)
|
|
2683
2762
|
if summary_gate:
|
|
2684
|
-
|
|
2685
|
-
self.
|
|
2686
|
-
|
|
2763
|
+
consecutive_summary_gates += 1
|
|
2764
|
+
if consecutive_summary_gates <= self.MAX_CONSECUTIVE_SUMMARY_GATES:
|
|
2765
|
+
self.state_updater.latest_report = ""
|
|
2766
|
+
self.latest_agent_feedback = summary_gate
|
|
2767
|
+
self._report_gate(
|
|
2768
|
+
on_message,
|
|
2769
|
+
"Retrying: model needs to summarize the latest tool results.",
|
|
2770
|
+
self._compact_gate_report(summary_gate),
|
|
2771
|
+
)
|
|
2772
|
+
continue
|
|
2773
|
+
self._report_gate(
|
|
2774
|
+
on_message,
|
|
2775
|
+
"Continuing: model did not summarize tool results after one retry.",
|
|
2776
|
+
"Tool_Summary_Gate: allowing continuation after one missing-summary retry.",
|
|
2777
|
+
)
|
|
2778
|
+
else:
|
|
2779
|
+
consecutive_summary_gates = 0
|
|
2687
2780
|
self.apply_response(response)
|
|
2688
2781
|
if on_message is not None and self.state_updater.latest_report:
|
|
2689
2782
|
on_message(self.state_updater.latest_report)
|
|
@@ -2704,14 +2797,36 @@ class Agent:
|
|
|
2704
2797
|
return response
|
|
2705
2798
|
if self.session.current.verification.status == VerificationStatus.REQUIRED:
|
|
2706
2799
|
self.session.current.goal_reached = False
|
|
2707
|
-
self.
|
|
2800
|
+
self.latest_agent_feedback = self._format_verification_gate()
|
|
2801
|
+
self._report_gate(
|
|
2802
|
+
on_message,
|
|
2803
|
+
"Retrying: verification is required before completion.",
|
|
2804
|
+
"Verification_Gate: retrying until verification is passed or blocked.",
|
|
2805
|
+
)
|
|
2708
2806
|
continue
|
|
2709
2807
|
if not self.session.current.goal_reached:
|
|
2710
|
-
self.
|
|
2808
|
+
self.latest_agent_feedback = self._format_continuation_hint()
|
|
2809
|
+
self._report_gate(
|
|
2810
|
+
on_message,
|
|
2811
|
+
"Continuing: goal is not complete yet.",
|
|
2812
|
+
"Continuation_Gate: goal not reached; retrying next useful action.",
|
|
2813
|
+
)
|
|
2711
2814
|
continue
|
|
2712
2815
|
return response
|
|
2713
2816
|
raise LLMError("agent step limit reached")
|
|
2714
2817
|
|
|
2818
|
+
def _report_gate(self, on_message: MessageCallback | None, message: str, debug_message: str) -> None:
|
|
2819
|
+
if on_message is not None:
|
|
2820
|
+
on_message(debug_message if self.session.debug else message)
|
|
2821
|
+
|
|
2822
|
+
def _compact_gate_report(self, gate: str) -> str:
|
|
2823
|
+
lines = gate.splitlines()
|
|
2824
|
+
headline = lines[0] if lines else "Gate"
|
|
2825
|
+
details = [line for line in lines[1:] if line.startswith("- ")]
|
|
2826
|
+
if details:
|
|
2827
|
+
return headline + ": " + _shorten("; ".join(details[:3]), 220)
|
|
2828
|
+
return headline
|
|
2829
|
+
|
|
2715
2830
|
def step(self) -> Json:
|
|
2716
2831
|
response = self.request(self.build_system_prompt(), self.build_user_prompt(), activity="main")
|
|
2717
2832
|
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
|