nanocode-cli 0.2.1__tar.gz → 0.2.2__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.2}/PKG-INFO +1 -1
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/nanocode.py +100 -56
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/pyproject.toml +1 -1
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/LICENSE +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/MANIFEST.in +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/README.md +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.2.1 → nanocode_cli-0.2.2}/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.2"
|
|
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
|
|
|
@@ -2092,25 +2138,32 @@ class ToolCallRunner:
|
|
|
2092
2138
|
if call is None:
|
|
2093
2139
|
call = self._invalid_tool_call(item)
|
|
2094
2140
|
|
|
2095
|
-
|
|
2141
|
+
log_written = not self._is_tool_result_file_read(call)
|
|
2142
|
+
if log_written:
|
|
2143
|
+
result_file, result_file_lines = self._write_tool_result_log(call, outcome, output)
|
|
2144
|
+
else:
|
|
2145
|
+
result_file = os.path.relpath(self.session.resolve_path(call.args[0]), self.session.cwd)
|
|
2146
|
+
result_file_lines = len(output.splitlines())
|
|
2096
2147
|
execution = ToolCallExecution(
|
|
2097
2148
|
call=call,
|
|
2098
2149
|
outcome=outcome,
|
|
2099
2150
|
output=output,
|
|
2100
2151
|
result_file=result_file,
|
|
2101
2152
|
result_file_lines=result_file_lines,
|
|
2153
|
+
log_written=log_written,
|
|
2102
2154
|
)
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2155
|
+
if log_written:
|
|
2156
|
+
event = ToolCallEvent(
|
|
2157
|
+
intent=call.intention,
|
|
2158
|
+
executed=call.executed,
|
|
2159
|
+
outcome=outcome,
|
|
2160
|
+
summary="",
|
|
2161
|
+
result_file=result_file,
|
|
2162
|
+
result_file_lines=result_file_lines,
|
|
2163
|
+
)
|
|
2164
|
+
self.session.append_conversation(event)
|
|
2165
|
+
events.append(event)
|
|
2112
2166
|
executions.append(execution)
|
|
2113
|
-
events.append(event)
|
|
2114
2167
|
|
|
2115
2168
|
self.latest_events = events
|
|
2116
2169
|
self.latest_executions = executions
|
|
@@ -2128,7 +2181,8 @@ class ToolCallRunner:
|
|
|
2128
2181
|
lines.append(" " + str(index) + ". [" + execution.outcome + "] " + execution.call.executed)
|
|
2129
2182
|
if execution.call.intention:
|
|
2130
2183
|
lines.append(" why: " + execution.call.intention)
|
|
2131
|
-
|
|
2184
|
+
label = "log" if execution.log_written else "source"
|
|
2185
|
+
lines.append(" " + label + ": " + execution.result_file + " (" + str(execution.result_file_lines) + " lines)")
|
|
2132
2186
|
return "\n".join(lines)
|
|
2133
2187
|
|
|
2134
2188
|
def parse_tool_call(self, value: JsonValue) -> ParsedToolCall:
|
|
@@ -2153,6 +2207,16 @@ class ToolCallRunner:
|
|
|
2153
2207
|
raise ToolCallError("tool not found: " + call.name)
|
|
2154
2208
|
return tool_class.make(self.session, call.args)
|
|
2155
2209
|
|
|
2210
|
+
def _is_tool_result_file_read(self, call: ParsedToolCall) -> bool:
|
|
2211
|
+
if call.name != ReadTool.name() or not call.args:
|
|
2212
|
+
return False
|
|
2213
|
+
tool_results_dir = os.path.realpath(self.session.tool_results_dir())
|
|
2214
|
+
path = os.path.realpath(self.session.resolve_path(call.args[0]))
|
|
2215
|
+
try:
|
|
2216
|
+
return os.path.commonpath([tool_results_dir, path]) == tool_results_dir
|
|
2217
|
+
except ValueError:
|
|
2218
|
+
return False
|
|
2219
|
+
|
|
2156
2220
|
def _write_tool_result_log(self, call: ParsedToolCall, outcome: str, output: str) -> tuple[str, int]:
|
|
2157
2221
|
directory = self.session.tool_results_dir()
|
|
2158
2222
|
os.makedirs(directory, exist_ok=True)
|
|
@@ -2563,9 +2627,6 @@ class ConversationCompactor:
|
|
|
2563
2627
|
|
|
2564
2628
|
@final
|
|
2565
2629
|
class Agent:
|
|
2566
|
-
EVIDENCE_OUTPUT_LINES: ClassVar[int] = 40
|
|
2567
|
-
EVIDENCE_OUTPUT_CHARS: ClassVar[int] = 4000
|
|
2568
|
-
|
|
2569
2630
|
def __init__(self, session: Session):
|
|
2570
2631
|
self.session = session
|
|
2571
2632
|
self.prompt_builder = PromptBuilder(session)
|
|
@@ -2688,49 +2749,32 @@ class Agent:
|
|
|
2688
2749
|
|
|
2689
2750
|
def _format_tool_summary_gate(self, tool_calls: list[JsonValue]) -> str:
|
|
2690
2751
|
missing = self._missing_tool_summaries()
|
|
2691
|
-
missing_evidence = []
|
|
2692
2752
|
needs_read = []
|
|
2693
|
-
for event
|
|
2753
|
+
for event in self.tool_runner.latest_events:
|
|
2694
2754
|
if not event.summary:
|
|
2695
2755
|
continue
|
|
2696
2756
|
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)
|
|
2757
|
+
if event.needs_raw_read:
|
|
2758
|
+
if not is_reading_result_file:
|
|
2759
|
+
needs_read.append(event)
|
|
2702
2760
|
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:
|
|
2761
|
+
if not missing and not needs_read:
|
|
2711
2762
|
return ""
|
|
2712
2763
|
|
|
2713
|
-
lines = ["Tool_Summary_Gate:
|
|
2764
|
+
lines = ["Tool_Summary_Gate: summarize latest tool results before continuing.", "Raw tool results are visible only once."]
|
|
2714
2765
|
if missing:
|
|
2715
2766
|
lines.append("Missing summaries:")
|
|
2716
2767
|
for event in missing:
|
|
2717
2768
|
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
2769
|
if needs_read:
|
|
2723
2770
|
lines.append("Needs raw read:")
|
|
2724
2771
|
for event in needs_read:
|
|
2725
2772
|
lines.append("- Read(" + event.result_file + ") before continuing")
|
|
2726
2773
|
lines.append(
|
|
2727
|
-
"Next_Action: return last_tool_calls_summaries
|
|
2774
|
+
"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
2775
|
)
|
|
2729
2776
|
return "\n".join(lines)
|
|
2730
2777
|
|
|
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
2778
|
def _has_read_result_file_call(self, tool_calls: list[JsonValue], result_file: str) -> bool:
|
|
2735
2779
|
if not result_file:
|
|
2736
2780
|
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
|