nanocode-cli 0.2.5__tar.gz → 0.2.7__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.5/nanocode_cli.egg-info → nanocode_cli-0.2.7}/PKG-INFO +1 -1
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/nanocode.py +155 -20
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/pyproject.toml +1 -1
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/LICENSE +0 -0
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/MANIFEST.in +0 -0
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/README.md +0 -0
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.2.5 → nanocode_cli-0.2.7}/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.7"
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
class Error(Exception): ...
|
|
@@ -355,6 +355,9 @@ class RangeFingerprintStore:
|
|
|
355
355
|
f"fingerprint mismatch for range {start}:{end}: expected {fingerprint}, current {current_fingerprint}; "
|
|
356
356
|
f"call Read(filepath, {start}, {end}) and reuse that range fingerprint"
|
|
357
357
|
)
|
|
358
|
+
other_ranges = self._ranges_for_fingerprint(filepath=filepath, fingerprint=fingerprint)
|
|
359
|
+
if other_ranges:
|
|
360
|
+
message += "; this fingerprint was cached for exact range(s): " + ", ".join(f"{range_start}:{range_end}" for range_start, range_end in other_ranges)
|
|
358
361
|
if not matches:
|
|
359
362
|
raise ToolCallError(message)
|
|
360
363
|
if len(matches) > 1:
|
|
@@ -389,6 +392,17 @@ class RangeFingerprintStore:
|
|
|
389
392
|
return matches
|
|
390
393
|
return matches
|
|
391
394
|
|
|
395
|
+
def _ranges_for_fingerprint(self, *, filepath: str, fingerprint: str) -> list[tuple[int, int]]:
|
|
396
|
+
filepath = os.path.realpath(filepath)
|
|
397
|
+
ranges = []
|
|
398
|
+
for entry in self._entries:
|
|
399
|
+
if entry.fingerprint != fingerprint or entry.filepath != filepath:
|
|
400
|
+
continue
|
|
401
|
+
item = (entry.start, entry.end)
|
|
402
|
+
if item not in ranges:
|
|
403
|
+
ranges.append(item)
|
|
404
|
+
return ranges
|
|
405
|
+
|
|
392
406
|
|
|
393
407
|
@final
|
|
394
408
|
@dataclass
|
|
@@ -436,6 +450,7 @@ class Session:
|
|
|
436
450
|
current: Current = field(default_factory=Current)
|
|
437
451
|
conversation: list[ConversationItem] = field(default_factory=list)
|
|
438
452
|
range_fingerprints: RangeFingerprintStore = field(default_factory=RangeFingerprintStore)
|
|
453
|
+
blackboard: list[str] = field(default_factory=list)
|
|
439
454
|
|
|
440
455
|
def resolve_path(self, path: str) -> str:
|
|
441
456
|
path = os.path.expanduser(path)
|
|
@@ -554,6 +569,13 @@ def _parse_line_range(start_arg: str, end_arg: str) -> tuple[int, int]:
|
|
|
554
569
|
return start, end
|
|
555
570
|
|
|
556
571
|
|
|
572
|
+
def _replacement_lines(content: str, *, has_following_line: bool) -> list[str]:
|
|
573
|
+
lines = content.splitlines(keepends=True)
|
|
574
|
+
if content and has_following_line and not content.endswith("\n"):
|
|
575
|
+
lines[-1] += "\n"
|
|
576
|
+
return lines
|
|
577
|
+
|
|
578
|
+
|
|
557
579
|
def _range_fingerprint(content: str) -> str:
|
|
558
580
|
return hashlib.blake2s(content.encode("utf-8"), digest_size=3).hexdigest()
|
|
559
581
|
|
|
@@ -585,7 +607,7 @@ class ReadTool(Tool):
|
|
|
585
607
|
"Optional range is 0-based [start,end); end=0 means EOF.",
|
|
586
608
|
"Returns at most 1000 lines; truncated results include total lines and next-step guidance.",
|
|
587
609
|
"Prefer Search before Read for large or unknown files; use bounded reads when exact context is needed.",
|
|
588
|
-
"For ReplaceRange,
|
|
610
|
+
"For ReplaceRange, the fingerprint is valid only for this exact filepath/start/end; read the exact edit range immediately before replacing.",
|
|
589
611
|
]
|
|
590
612
|
|
|
591
613
|
@classmethod
|
|
@@ -733,7 +755,7 @@ class ListDirTool(Tool):
|
|
|
733
755
|
|
|
734
756
|
@classmethod
|
|
735
757
|
def example(cls) -> list[str]:
|
|
736
|
-
return [
|
|
758
|
+
return ["Example args: []", 'Example args: ["."]', 'Example args: ["src", "*.py"]']
|
|
737
759
|
|
|
738
760
|
@classmethod
|
|
739
761
|
def make(cls, session: Session, args: list[str]) -> Self:
|
|
@@ -793,7 +815,7 @@ class SearchTool(Tool):
|
|
|
793
815
|
MAX_FILE_BYTES: ClassVar[int] = 2_000_000
|
|
794
816
|
RG_MAX_FILESIZE: ClassVar[str] = "2M"
|
|
795
817
|
CONTEXT_LINES: ClassVar[int] = 4
|
|
796
|
-
MAX_CONTEXT_LINES: ClassVar[int] =
|
|
818
|
+
MAX_CONTEXT_LINES: ClassVar[int] = 30
|
|
797
819
|
|
|
798
820
|
@dataclass(frozen=True)
|
|
799
821
|
class Match:
|
|
@@ -820,14 +842,15 @@ class SearchTool(Tool):
|
|
|
820
842
|
return [
|
|
821
843
|
"Search files or directories before Read; default is fixed text.",
|
|
822
844
|
"Prefix pattern with re: for regex search.",
|
|
845
|
+
"Search is line-oriented; regex patterns must not contain newlines.",
|
|
823
846
|
"Use A|B|C for literal OR search in fixed mode.",
|
|
824
|
-
"Optional context=N or N sets nearby context lines.",
|
|
847
|
+
"Optional context=N or N sets nearby context lines, from 0 to 30.",
|
|
825
848
|
"Optional glob matches file basename or path relative to cwd.",
|
|
826
849
|
]
|
|
827
850
|
|
|
828
851
|
@classmethod
|
|
829
852
|
def signature(cls) -> str:
|
|
830
|
-
return "Search(pattern, path[,
|
|
853
|
+
return "Search(pattern, path[, option...]) -> SearchToolResult<matches>; option is context=N|N (0..30) or glob_pattern"
|
|
831
854
|
|
|
832
855
|
@classmethod
|
|
833
856
|
def example(cls) -> list[str]:
|
|
@@ -850,6 +873,8 @@ class SearchTool(Tool):
|
|
|
850
873
|
pattern = raw_pattern[3:] if regex else raw_pattern
|
|
851
874
|
if not pattern:
|
|
852
875
|
raise ToolCallError("pattern cannot be empty")
|
|
876
|
+
if regex and "\n" in pattern:
|
|
877
|
+
raise ToolCallError("multiline regex is not supported; Search is line-oriented. Search each line separately or Read a nearby range.")
|
|
853
878
|
glob_pattern = ""
|
|
854
879
|
context_lines = cls.CONTEXT_LINES
|
|
855
880
|
for raw_option in args[2:]:
|
|
@@ -860,6 +885,10 @@ class SearchTool(Tool):
|
|
|
860
885
|
except ValueError:
|
|
861
886
|
raise ToolCallError("context must be an integer between 0 and " + str(cls.MAX_CONTEXT_LINES))
|
|
862
887
|
continue
|
|
888
|
+
if option.startswith("glob=") or option.startswith("glob_pattern="):
|
|
889
|
+
option = option.split("=", 1)[1]
|
|
890
|
+
if not option:
|
|
891
|
+
raise ToolCallError("glob option cannot be empty")
|
|
863
892
|
if glob_pattern:
|
|
864
893
|
raise ToolCallError("unexpected search option: " + option)
|
|
865
894
|
glob_pattern = option
|
|
@@ -1080,6 +1109,8 @@ class SearchTool(Tool):
|
|
|
1080
1109
|
|
|
1081
1110
|
def call(self) -> str:
|
|
1082
1111
|
if not (os.path.isdir(self.target_path) or os.path.isfile(self.target_path)):
|
|
1112
|
+
if os.path.basename(self.target_path) == "path":
|
|
1113
|
+
raise ToolCallError('not a file or directory: "path" is a placeholder; pass a real file or directory')
|
|
1083
1114
|
raise ToolCallError("not a file or directory")
|
|
1084
1115
|
if os.path.isfile(self.target_path) and not self._matches_glob(self.target_path):
|
|
1085
1116
|
return self._format_result("python", [], False)
|
|
@@ -1174,7 +1205,8 @@ class ReplaceRangeTool(Tool):
|
|
|
1174
1205
|
@classmethod
|
|
1175
1206
|
def description(cls) -> list[str]:
|
|
1176
1207
|
return [
|
|
1177
|
-
"Replace one 0-based line range when its fingerprint comes from Read(filepath, same start, same end).",
|
|
1208
|
+
"Replace one 0-based line range when its fingerprint comes from Read(filepath, exact same start, exact same end).",
|
|
1209
|
+
"Never use a wider Read fingerprint for a narrower edit; if mismatch happens, Read the exact target range and retry once.",
|
|
1178
1210
|
"If earlier edits shifted lines, a cached Read fingerprint for the same original range can relocate only when old content still matches exactly once.",
|
|
1179
1211
|
]
|
|
1180
1212
|
|
|
@@ -1215,6 +1247,13 @@ class ReplaceRangeTool(Tool):
|
|
|
1215
1247
|
return label + "\n# preview unavailable: " + str(error)
|
|
1216
1248
|
return _make_unified_diff(original, new_content, self.filepath) or label
|
|
1217
1249
|
|
|
1250
|
+
def preview_error(self) -> str:
|
|
1251
|
+
try:
|
|
1252
|
+
self._preview()
|
|
1253
|
+
except (OSError, ToolCallError) as error:
|
|
1254
|
+
return str(error)
|
|
1255
|
+
return ""
|
|
1256
|
+
|
|
1218
1257
|
def call(self) -> str:
|
|
1219
1258
|
original, new_content, resolved, _ = self._preview()
|
|
1220
1259
|
if new_content == original:
|
|
@@ -1245,7 +1284,7 @@ class ReplaceRangeTool(Tool):
|
|
|
1245
1284
|
end=self.end,
|
|
1246
1285
|
fingerprint=self.fingerprint,
|
|
1247
1286
|
)
|
|
1248
|
-
replacement = self.content.
|
|
1287
|
+
replacement = _replacement_lines(self.content, has_following_line=resolved.end < len(lines))
|
|
1249
1288
|
new_lines = lines[: resolved.start] + replacement + lines[resolved.end :]
|
|
1250
1289
|
return original, "".join(new_lines), resolved, replacement
|
|
1251
1290
|
|
|
@@ -1275,7 +1314,8 @@ class BatchReplaceRangesTool(Tool):
|
|
|
1275
1314
|
return [
|
|
1276
1315
|
"Replace multiple 0-based line ranges in one file against one snapshot.",
|
|
1277
1316
|
"Use this for multiple edits in the same file; earlier edits in this call do not shift later ranges.",
|
|
1278
|
-
"Each edit fingerprint must come from Read(filepath, same start, same end);
|
|
1317
|
+
"Each edit fingerprint must come from Read(filepath, exact same start, exact same end); never reuse a wider Read fingerprint for a narrower edit.",
|
|
1318
|
+
"Same-range cached fingerprints can relocate shifted old content.",
|
|
1279
1319
|
]
|
|
1280
1320
|
|
|
1281
1321
|
@classmethod
|
|
@@ -1328,6 +1368,13 @@ class BatchReplaceRangesTool(Tool):
|
|
|
1328
1368
|
return label + "\n# preview unavailable: " + str(error)
|
|
1329
1369
|
return _make_unified_diff(original, new_content, self.filepath) or label
|
|
1330
1370
|
|
|
1371
|
+
def preview_error(self) -> str:
|
|
1372
|
+
try:
|
|
1373
|
+
self._preview()
|
|
1374
|
+
except (OSError, ToolCallError) as error:
|
|
1375
|
+
return str(error)
|
|
1376
|
+
return ""
|
|
1377
|
+
|
|
1331
1378
|
def call(self) -> str:
|
|
1332
1379
|
original, new_content, resolved_edits = self._preview()
|
|
1333
1380
|
if new_content == original:
|
|
@@ -1362,7 +1409,7 @@ class BatchReplaceRangesTool(Tool):
|
|
|
1362
1409
|
end=edit.end,
|
|
1363
1410
|
fingerprint=edit.fingerprint,
|
|
1364
1411
|
)
|
|
1365
|
-
resolved_edits.append((resolved, edit.content.
|
|
1412
|
+
resolved_edits.append((resolved, _replacement_lines(edit.content, has_following_line=resolved.end < len(lines))))
|
|
1366
1413
|
self._ensure_non_overlapping(resolved_edits)
|
|
1367
1414
|
|
|
1368
1415
|
new_lines = list(lines)
|
|
@@ -1674,6 +1721,67 @@ class GitTool(Tool):
|
|
|
1674
1721
|
return _format_process_result("GitToolResult", -1, error.stdout or "", (error.stderr or "") + "timeout")
|
|
1675
1722
|
|
|
1676
1723
|
|
|
1724
|
+
@final
|
|
1725
|
+
@dataclass
|
|
1726
|
+
class BlackboardTool(Tool):
|
|
1727
|
+
action: str
|
|
1728
|
+
content: str
|
|
1729
|
+
blackboard: list[str]
|
|
1730
|
+
|
|
1731
|
+
@classmethod
|
|
1732
|
+
def name(cls) -> str:
|
|
1733
|
+
return "Blackboard"
|
|
1734
|
+
|
|
1735
|
+
@classmethod
|
|
1736
|
+
def description(cls) -> list[str]:
|
|
1737
|
+
return [
|
|
1738
|
+
"Scratchpad for hypotheses, intermediate analysis, or task state.",
|
|
1739
|
+
"Stores temporary data outside main context; cleared when the session ends. Actions: read, append, clear.",
|
|
1740
|
+
]
|
|
1741
|
+
|
|
1742
|
+
@classmethod
|
|
1743
|
+
def signature(cls) -> str:
|
|
1744
|
+
return "Blackboard(action[, content]) -> BlackboardToolResult<content>"
|
|
1745
|
+
|
|
1746
|
+
@classmethod
|
|
1747
|
+
def example(cls) -> list[str]:
|
|
1748
|
+
return [
|
|
1749
|
+
'{"name": "Blackboard", "intention": "Record progress", "args": ["append", "Step 1 done"]}',
|
|
1750
|
+
'{"name": "Blackboard", "intention": "Clear it", "args": ["clear"]}',
|
|
1751
|
+
]
|
|
1752
|
+
|
|
1753
|
+
@classmethod
|
|
1754
|
+
def make(cls, session: Session, args: list[str]) -> Self:
|
|
1755
|
+
action = args[0] if args else "read"
|
|
1756
|
+
content = args[1] if len(args) > 1 else ""
|
|
1757
|
+
return cls(action=action, content=content, blackboard=session.blackboard)
|
|
1758
|
+
|
|
1759
|
+
def requires_confirmation(self, session: Session) -> bool:
|
|
1760
|
+
return False
|
|
1761
|
+
|
|
1762
|
+
def display(self) -> str:
|
|
1763
|
+
if self.action == "append":
|
|
1764
|
+
preview = self.content.replace("\n", " ")[:80]
|
|
1765
|
+
return f"Blackboard append: {preview}"
|
|
1766
|
+
return f"Blackboard {self.action}"
|
|
1767
|
+
|
|
1768
|
+
def call(self) -> str:
|
|
1769
|
+
if self.action == "read":
|
|
1770
|
+
content = "\n".join(self.blackboard)
|
|
1771
|
+
return f"<BlackboardToolResult>\n{content}\n</BlackboardToolResult>"
|
|
1772
|
+
|
|
1773
|
+
if self.action == "append":
|
|
1774
|
+
if self.content:
|
|
1775
|
+
self.blackboard.append(self.content.rstrip())
|
|
1776
|
+
return "<BlackboardToolResult>appended</BlackboardToolResult>"
|
|
1777
|
+
|
|
1778
|
+
if self.action == "clear":
|
|
1779
|
+
self.blackboard.clear()
|
|
1780
|
+
return "<BlackboardToolResult>cleared</BlackboardToolResult>"
|
|
1781
|
+
|
|
1782
|
+
raise ToolCallError("Blackboard action must be one of: read, append, clear")
|
|
1783
|
+
|
|
1784
|
+
|
|
1677
1785
|
TOOL_REGISTRY: dict[str, ToolClass] = {
|
|
1678
1786
|
ReadTool.name(): ReadTool,
|
|
1679
1787
|
LineCountTool.name(): LineCountTool,
|
|
@@ -1685,13 +1793,14 @@ TOOL_REGISTRY: dict[str, ToolClass] = {
|
|
|
1685
1793
|
ApplyPatchTool.name(): ApplyPatchTool,
|
|
1686
1794
|
BashTool.name(): BashTool,
|
|
1687
1795
|
GitTool.name(): GitTool,
|
|
1796
|
+
BlackboardTool.name(): BlackboardTool,
|
|
1688
1797
|
}
|
|
1689
1798
|
|
|
1690
1799
|
|
|
1691
1800
|
#######################
|
|
1692
1801
|
# Prompt
|
|
1693
|
-
#######################
|
|
1694
1802
|
|
|
1803
|
+
#######################
|
|
1695
1804
|
MAIN_AGENT_SYSTEM_PROMPT = """You are nanocode, a minimal coding agent.
|
|
1696
1805
|
|
|
1697
1806
|
Core:
|
|
@@ -1709,6 +1818,7 @@ Tools:
|
|
|
1709
1818
|
- Prefer specific tools first; use Bash only when no provided tool fits.
|
|
1710
1819
|
- Prefer Search before Read when locating code or facts; Read only known small ranges or exact files needed for editing.
|
|
1711
1820
|
- Read returns at most 1000 lines; if truncated, use Search or smaller Read ranges in batches.
|
|
1821
|
+
- ReplaceRange/BatchReplaceRanges fingerprints are valid only for the exact filepath/start/end returned by Read. Never use a wider Read fingerprint for a narrower edit. If fingerprint mismatch happens, immediately Read the exact target range and retry once.
|
|
1712
1822
|
- 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
1823
|
- Latest tool results are already shown in Latest_Tool_Call_Results; use result_file logs only as a fallback when needed.
|
|
1714
1824
|
- 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.
|
|
@@ -1968,6 +2078,7 @@ class ModelClient:
|
|
|
1968
2078
|
"model": self.session.model,
|
|
1969
2079
|
"messages": messages,
|
|
1970
2080
|
"temperature": self.session.temperature,
|
|
2081
|
+
"response_format": {"type": "json_object"},
|
|
1971
2082
|
}
|
|
1972
2083
|
extra_params = self._reasoning_params()
|
|
1973
2084
|
payload.update(extra_params)
|
|
@@ -2164,6 +2275,9 @@ class ToolCallRunner:
|
|
|
2164
2275
|
try:
|
|
2165
2276
|
call = self.parse_tool_call(item)
|
|
2166
2277
|
tool = self._make_tool(call)
|
|
2278
|
+
preview_error = self._preview_error(tool)
|
|
2279
|
+
if preview_error:
|
|
2280
|
+
raise ToolCallError("preview unavailable: " + preview_error)
|
|
2167
2281
|
if tool.requires_confirmation(self.session):
|
|
2168
2282
|
if self.session.yolo:
|
|
2169
2283
|
if on_auto_approve is not None:
|
|
@@ -2256,6 +2370,12 @@ class ToolCallRunner:
|
|
|
2256
2370
|
raise ToolCallError("tool not found: " + call.name)
|
|
2257
2371
|
return tool_class.make(self.session, call.args)
|
|
2258
2372
|
|
|
2373
|
+
def _preview_error(self, tool: Tool) -> str:
|
|
2374
|
+
preview_error = getattr(tool, "preview_error", None)
|
|
2375
|
+
if not callable(preview_error):
|
|
2376
|
+
return ""
|
|
2377
|
+
return str(preview_error())
|
|
2378
|
+
|
|
2259
2379
|
def _is_tool_result_file_read(self, call: ParsedToolCall) -> bool:
|
|
2260
2380
|
if call.name != ReadTool.name() or not call.args:
|
|
2261
2381
|
return False
|
|
@@ -2736,19 +2856,14 @@ class Agent:
|
|
|
2736
2856
|
if consecutive_format_errors >= self.MAX_CONSECUTIVE_FORMAT_ERRORS:
|
|
2737
2857
|
self._report_gate(
|
|
2738
2858
|
on_message,
|
|
2739
|
-
"Stopped: model returned invalid output "
|
|
2740
|
-
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2741
|
-
+ " times in a row.",
|
|
2859
|
+
"Stopped: model returned invalid output " + str(self.MAX_CONSECUTIVE_FORMAT_ERRORS) + " times in a row.",
|
|
2742
2860
|
"Format_Gate: stopped after "
|
|
2743
2861
|
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2744
2862
|
+ " consecutive invalid model outputs. "
|
|
2745
2863
|
+ _shorten(format_error, 180),
|
|
2746
2864
|
)
|
|
2747
2865
|
raise LLMError(
|
|
2748
|
-
"model returned invalid output "
|
|
2749
|
-
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2750
|
-
+ " times in a row: "
|
|
2751
|
-
+ _shorten(format_error, 300)
|
|
2866
|
+
"model returned invalid output " + str(self.MAX_CONSECUTIVE_FORMAT_ERRORS) + " times in a row: " + _shorten(format_error, 300)
|
|
2752
2867
|
)
|
|
2753
2868
|
self._report_gate(
|
|
2754
2869
|
on_message,
|
|
@@ -2809,7 +2924,11 @@ class Agent:
|
|
|
2809
2924
|
return headline
|
|
2810
2925
|
|
|
2811
2926
|
def step(self) -> Json:
|
|
2812
|
-
response = self.request(self.build_system_prompt(), self.build_user_prompt(), activity="main")
|
|
2927
|
+
response = self.request(self.build_system_prompt(), self.build_user_prompt(consume_latest_tool_results=False), activity="main")
|
|
2928
|
+
if _json_str(response.get("_format_error")):
|
|
2929
|
+
return response
|
|
2930
|
+
self.latest_tool_call_results = ""
|
|
2931
|
+
self.latest_agent_feedback = ""
|
|
2813
2932
|
self.state_updater.apply_tool_call_summaries(response)
|
|
2814
2933
|
return response
|
|
2815
2934
|
|
|
@@ -2926,6 +3045,7 @@ COMMANDS: tuple[CommandSpec, ...] = (
|
|
|
2926
3045
|
CommandSpec("/yolo", "Show or toggle confirmation bypass", "Config", "/yolo [on|off|status]"),
|
|
2927
3046
|
CommandSpec("/exit", "Exit nanocode", "Control", "/exit"),
|
|
2928
3047
|
CommandSpec("/quit", "Exit nanocode", "Control", "/quit"),
|
|
3048
|
+
CommandSpec("/blackboard", "View or clear the blackboard", "Control", "/blackboard [status|clear]"),
|
|
2929
3049
|
)
|
|
2930
3050
|
|
|
2931
3051
|
|
|
@@ -2942,6 +3062,7 @@ class CommandDispatcher:
|
|
|
2942
3062
|
self.agent = agent
|
|
2943
3063
|
self.run_agent = run_agent
|
|
2944
3064
|
self.run_with_status = run_with_status
|
|
3065
|
+
self.blackboard = agent.session.blackboard
|
|
2945
3066
|
self.handlers: dict[str, Callable[[str], str]] = {
|
|
2946
3067
|
"/help": self._help,
|
|
2947
3068
|
"/status": self._status,
|
|
@@ -2951,6 +3072,7 @@ class CommandDispatcher:
|
|
|
2951
3072
|
"/reason": self._reason,
|
|
2952
3073
|
"/reason_effort": self._reason_effort,
|
|
2953
3074
|
"/yolo": self._yolo,
|
|
3075
|
+
"/blackboard": self._blackboard,
|
|
2954
3076
|
}
|
|
2955
3077
|
|
|
2956
3078
|
def dispatch(self, user_input: str) -> CommandResult:
|
|
@@ -3015,6 +3137,7 @@ class CommandDispatcher:
|
|
|
3015
3137
|
"yolo: " + yolo,
|
|
3016
3138
|
"conversation: " + str(len(session.conversation)) + "/" + str(session.compact_at),
|
|
3017
3139
|
"tokens: last=" + _format_count(session.last_total_tokens) + " session=" + _format_count(session.session_total_tokens),
|
|
3140
|
+
"blackboard: " + str(len(session.blackboard)) + " items",
|
|
3018
3141
|
"goal: " + (session.current.goal or "(empty)"),
|
|
3019
3142
|
"verification: " + session.current.verification.status,
|
|
3020
3143
|
]
|
|
@@ -3086,6 +3209,17 @@ class CommandDispatcher:
|
|
|
3086
3209
|
return "YOLO is " + ("on" if self.agent.session.yolo else "off")
|
|
3087
3210
|
return "Usage: /yolo [on|off|status]"
|
|
3088
3211
|
|
|
3212
|
+
def _blackboard(self, args: str) -> str:
|
|
3213
|
+
if args == "clear":
|
|
3214
|
+
self.blackboard.clear()
|
|
3215
|
+
return "Blackboard cleared"
|
|
3216
|
+
if args in {"", "status"}:
|
|
3217
|
+
content = "\n".join(self.blackboard)
|
|
3218
|
+
if content:
|
|
3219
|
+
return "Blackboard:\n" + content
|
|
3220
|
+
return "Blackboard is empty"
|
|
3221
|
+
return "Usage: /blackboard [status|clear]"
|
|
3222
|
+
|
|
3089
3223
|
|
|
3090
3224
|
def _format_count(value: int) -> str:
|
|
3091
3225
|
if value <= 0:
|
|
@@ -3193,7 +3327,8 @@ class StatusBar:
|
|
|
3193
3327
|
yolo = " | yolo" if session.yolo else ""
|
|
3194
3328
|
context = str(len(session.conversation)) + "/" + str(session.compact_at)
|
|
3195
3329
|
tokens = "last:" + self._format_count(session.last_total_tokens) + " session:" + self._format_count(session.session_total_tokens)
|
|
3196
|
-
|
|
3330
|
+
blackboard = "bb:" + str(len(session.blackboard))
|
|
3331
|
+
parts = [model + " (" + reasoning + ")" + yolo, "ctx:" + context, "tok:" + tokens, blackboard]
|
|
3197
3332
|
if show_elapsed:
|
|
3198
3333
|
parts.append(f"{turn_elapsed:.1f}s")
|
|
3199
3334
|
if session.current_model_call_started_at > 0:
|
|
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
|