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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: A lightweight terminal-based AI coding assistant
5
5
  Author-email: hit9 <hit9@icloud.com>
6
6
  License-Expression: BSD-3-Clause
@@ -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.1"
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
- "Prefer bounded reads over full-file reads; use LineCount first when unsure.",
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
- with open(self.filepath, "r", encoding="utf-8") as f:
611
- lines = itertools.islice(f, self.start, self.end or None)
612
- content = "".join(lines)
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=self.end,
639
+ end=fingerprint_end,
617
640
  content=content,
618
641
  )
619
- return "\n".join(
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 "ListDir(dir_path[, glob_pattern]) -> ListDirToolResult<entries>"
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 1 or 2 args: dir_path[, glob_pattern]")
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(args[0]), glob_pattern=glob_pattern, cwd=session.cwd)
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
- - Summarize every latest tool result in last_tool_calls_summaries; raw results are shown once only, so key_evidence keeps paths, lines, errors, decisions.
1668
- - If a prior tool result lacks detail, use Read on its result_file.
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
- result_file, result_file_lines = self._write_tool_result_log(call, outcome, output)
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
- event = ToolCallEvent(
2104
- intent=call.intention,
2105
- executed=call.executed,
2106
- outcome=outcome,
2107
- summary="",
2108
- result_file=result_file,
2109
- result_file_lines=result_file_lines,
2110
- )
2111
- self.session.append_conversation(event)
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
- lines.append(" log: " + execution.result_file + " (" + str(execution.result_file_lines) + " lines)")
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, execution in zip(self.tool_runner.latest_events, self.tool_runner.latest_executions):
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 and not is_reading_result_file:
2698
- needs_read.append(event)
2699
- if event.outcome in {"failure", "partial"}:
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
- if (
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: extract durable evidence before continuing.", "Raw tool results are visible only once."]
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 with key_evidence, update current_context_update for task-local facts, or call Read(result_file) for needs_raw_read logs."
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: A lightweight terminal-based AI coding assistant
5
5
  Author-email: hit9 <hit9@icloud.com>
6
6
  License-Expression: BSD-3-Clause
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nanocode-cli"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "A lightweight terminal-based AI coding assistant"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes