nanocode-cli 0.2.6__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.6/nanocode_cli.egg-info → nanocode_cli-0.2.7}/PKG-INFO +1 -1
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7}/nanocode.py +146 -19
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7/nanocode_cli.egg-info}/PKG-INFO +1 -1
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7}/pyproject.toml +1 -1
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7}/LICENSE +0 -0
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7}/MANIFEST.in +0 -0
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7}/README.md +0 -0
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7}/nanocode_cli.egg-info/SOURCES.txt +0 -0
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7}/nanocode_cli.egg-info/dependency_links.txt +0 -0
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7}/nanocode_cli.egg-info/entry_points.txt +0 -0
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7}/nanocode_cli.egg-info/requires.txt +0 -0
- {nanocode_cli-0.2.6 → nanocode_cli-0.2.7}/nanocode_cli.egg-info/top_level.txt +0 -0
- {nanocode_cli-0.2.6 → 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:
|
|
@@ -822,13 +844,13 @@ class SearchTool(Tool):
|
|
|
822
844
|
"Prefix pattern with re: for regex search.",
|
|
823
845
|
"Search is line-oriented; regex patterns must not contain newlines.",
|
|
824
846
|
"Use A|B|C for literal OR search in fixed mode.",
|
|
825
|
-
"Optional context=N or N sets nearby context lines.",
|
|
847
|
+
"Optional context=N or N sets nearby context lines, from 0 to 30.",
|
|
826
848
|
"Optional glob matches file basename or path relative to cwd.",
|
|
827
849
|
]
|
|
828
850
|
|
|
829
851
|
@classmethod
|
|
830
852
|
def signature(cls) -> str:
|
|
831
|
-
return "Search(pattern, path[, option...]) -> SearchToolResult<matches>; option is context=N|N or glob_pattern"
|
|
853
|
+
return "Search(pattern, path[, option...]) -> SearchToolResult<matches>; option is context=N|N (0..30) or glob_pattern"
|
|
832
854
|
|
|
833
855
|
@classmethod
|
|
834
856
|
def example(cls) -> list[str]:
|
|
@@ -863,6 +885,10 @@ class SearchTool(Tool):
|
|
|
863
885
|
except ValueError:
|
|
864
886
|
raise ToolCallError("context must be an integer between 0 and " + str(cls.MAX_CONTEXT_LINES))
|
|
865
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")
|
|
866
892
|
if glob_pattern:
|
|
867
893
|
raise ToolCallError("unexpected search option: " + option)
|
|
868
894
|
glob_pattern = option
|
|
@@ -1083,6 +1109,8 @@ class SearchTool(Tool):
|
|
|
1083
1109
|
|
|
1084
1110
|
def call(self) -> str:
|
|
1085
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')
|
|
1086
1114
|
raise ToolCallError("not a file or directory")
|
|
1087
1115
|
if os.path.isfile(self.target_path) and not self._matches_glob(self.target_path):
|
|
1088
1116
|
return self._format_result("python", [], False)
|
|
@@ -1177,7 +1205,8 @@ class ReplaceRangeTool(Tool):
|
|
|
1177
1205
|
@classmethod
|
|
1178
1206
|
def description(cls) -> list[str]:
|
|
1179
1207
|
return [
|
|
1180
|
-
"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.",
|
|
1181
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.",
|
|
1182
1211
|
]
|
|
1183
1212
|
|
|
@@ -1218,6 +1247,13 @@ class ReplaceRangeTool(Tool):
|
|
|
1218
1247
|
return label + "\n# preview unavailable: " + str(error)
|
|
1219
1248
|
return _make_unified_diff(original, new_content, self.filepath) or label
|
|
1220
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
|
+
|
|
1221
1257
|
def call(self) -> str:
|
|
1222
1258
|
original, new_content, resolved, _ = self._preview()
|
|
1223
1259
|
if new_content == original:
|
|
@@ -1248,7 +1284,7 @@ class ReplaceRangeTool(Tool):
|
|
|
1248
1284
|
end=self.end,
|
|
1249
1285
|
fingerprint=self.fingerprint,
|
|
1250
1286
|
)
|
|
1251
|
-
replacement = self.content.
|
|
1287
|
+
replacement = _replacement_lines(self.content, has_following_line=resolved.end < len(lines))
|
|
1252
1288
|
new_lines = lines[: resolved.start] + replacement + lines[resolved.end :]
|
|
1253
1289
|
return original, "".join(new_lines), resolved, replacement
|
|
1254
1290
|
|
|
@@ -1278,7 +1314,8 @@ class BatchReplaceRangesTool(Tool):
|
|
|
1278
1314
|
return [
|
|
1279
1315
|
"Replace multiple 0-based line ranges in one file against one snapshot.",
|
|
1280
1316
|
"Use this for multiple edits in the same file; earlier edits in this call do not shift later ranges.",
|
|
1281
|
-
"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.",
|
|
1282
1319
|
]
|
|
1283
1320
|
|
|
1284
1321
|
@classmethod
|
|
@@ -1331,6 +1368,13 @@ class BatchReplaceRangesTool(Tool):
|
|
|
1331
1368
|
return label + "\n# preview unavailable: " + str(error)
|
|
1332
1369
|
return _make_unified_diff(original, new_content, self.filepath) or label
|
|
1333
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
|
+
|
|
1334
1378
|
def call(self) -> str:
|
|
1335
1379
|
original, new_content, resolved_edits = self._preview()
|
|
1336
1380
|
if new_content == original:
|
|
@@ -1365,7 +1409,7 @@ class BatchReplaceRangesTool(Tool):
|
|
|
1365
1409
|
end=edit.end,
|
|
1366
1410
|
fingerprint=edit.fingerprint,
|
|
1367
1411
|
)
|
|
1368
|
-
resolved_edits.append((resolved, edit.content.
|
|
1412
|
+
resolved_edits.append((resolved, _replacement_lines(edit.content, has_following_line=resolved.end < len(lines))))
|
|
1369
1413
|
self._ensure_non_overlapping(resolved_edits)
|
|
1370
1414
|
|
|
1371
1415
|
new_lines = list(lines)
|
|
@@ -1677,6 +1721,67 @@ class GitTool(Tool):
|
|
|
1677
1721
|
return _format_process_result("GitToolResult", -1, error.stdout or "", (error.stderr or "") + "timeout")
|
|
1678
1722
|
|
|
1679
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
|
+
|
|
1680
1785
|
TOOL_REGISTRY: dict[str, ToolClass] = {
|
|
1681
1786
|
ReadTool.name(): ReadTool,
|
|
1682
1787
|
LineCountTool.name(): LineCountTool,
|
|
@@ -1688,13 +1793,14 @@ TOOL_REGISTRY: dict[str, ToolClass] = {
|
|
|
1688
1793
|
ApplyPatchTool.name(): ApplyPatchTool,
|
|
1689
1794
|
BashTool.name(): BashTool,
|
|
1690
1795
|
GitTool.name(): GitTool,
|
|
1796
|
+
BlackboardTool.name(): BlackboardTool,
|
|
1691
1797
|
}
|
|
1692
1798
|
|
|
1693
1799
|
|
|
1694
1800
|
#######################
|
|
1695
1801
|
# Prompt
|
|
1696
|
-
#######################
|
|
1697
1802
|
|
|
1803
|
+
#######################
|
|
1698
1804
|
MAIN_AGENT_SYSTEM_PROMPT = """You are nanocode, a minimal coding agent.
|
|
1699
1805
|
|
|
1700
1806
|
Core:
|
|
@@ -1712,6 +1818,7 @@ Tools:
|
|
|
1712
1818
|
- Prefer specific tools first; use Bash only when no provided tool fits.
|
|
1713
1819
|
- Prefer Search before Read when locating code or facts; Read only known small ranges or exact files needed for editing.
|
|
1714
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.
|
|
1715
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.
|
|
1716
1823
|
- Latest tool results are already shown in Latest_Tool_Call_Results; use result_file logs only as a fallback when needed.
|
|
1717
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.
|
|
@@ -2168,6 +2275,9 @@ class ToolCallRunner:
|
|
|
2168
2275
|
try:
|
|
2169
2276
|
call = self.parse_tool_call(item)
|
|
2170
2277
|
tool = self._make_tool(call)
|
|
2278
|
+
preview_error = self._preview_error(tool)
|
|
2279
|
+
if preview_error:
|
|
2280
|
+
raise ToolCallError("preview unavailable: " + preview_error)
|
|
2171
2281
|
if tool.requires_confirmation(self.session):
|
|
2172
2282
|
if self.session.yolo:
|
|
2173
2283
|
if on_auto_approve is not None:
|
|
@@ -2260,6 +2370,12 @@ class ToolCallRunner:
|
|
|
2260
2370
|
raise ToolCallError("tool not found: " + call.name)
|
|
2261
2371
|
return tool_class.make(self.session, call.args)
|
|
2262
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
|
+
|
|
2263
2379
|
def _is_tool_result_file_read(self, call: ParsedToolCall) -> bool:
|
|
2264
2380
|
if call.name != ReadTool.name() or not call.args:
|
|
2265
2381
|
return False
|
|
@@ -2740,19 +2856,14 @@ class Agent:
|
|
|
2740
2856
|
if consecutive_format_errors >= self.MAX_CONSECUTIVE_FORMAT_ERRORS:
|
|
2741
2857
|
self._report_gate(
|
|
2742
2858
|
on_message,
|
|
2743
|
-
"Stopped: model returned invalid output "
|
|
2744
|
-
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2745
|
-
+ " times in a row.",
|
|
2859
|
+
"Stopped: model returned invalid output " + str(self.MAX_CONSECUTIVE_FORMAT_ERRORS) + " times in a row.",
|
|
2746
2860
|
"Format_Gate: stopped after "
|
|
2747
2861
|
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2748
2862
|
+ " consecutive invalid model outputs. "
|
|
2749
2863
|
+ _shorten(format_error, 180),
|
|
2750
2864
|
)
|
|
2751
2865
|
raise LLMError(
|
|
2752
|
-
"model returned invalid output "
|
|
2753
|
-
+ str(self.MAX_CONSECUTIVE_FORMAT_ERRORS)
|
|
2754
|
-
+ " times in a row: "
|
|
2755
|
-
+ _shorten(format_error, 300)
|
|
2866
|
+
"model returned invalid output " + str(self.MAX_CONSECUTIVE_FORMAT_ERRORS) + " times in a row: " + _shorten(format_error, 300)
|
|
2756
2867
|
)
|
|
2757
2868
|
self._report_gate(
|
|
2758
2869
|
on_message,
|
|
@@ -2934,6 +3045,7 @@ COMMANDS: tuple[CommandSpec, ...] = (
|
|
|
2934
3045
|
CommandSpec("/yolo", "Show or toggle confirmation bypass", "Config", "/yolo [on|off|status]"),
|
|
2935
3046
|
CommandSpec("/exit", "Exit nanocode", "Control", "/exit"),
|
|
2936
3047
|
CommandSpec("/quit", "Exit nanocode", "Control", "/quit"),
|
|
3048
|
+
CommandSpec("/blackboard", "View or clear the blackboard", "Control", "/blackboard [status|clear]"),
|
|
2937
3049
|
)
|
|
2938
3050
|
|
|
2939
3051
|
|
|
@@ -2950,6 +3062,7 @@ class CommandDispatcher:
|
|
|
2950
3062
|
self.agent = agent
|
|
2951
3063
|
self.run_agent = run_agent
|
|
2952
3064
|
self.run_with_status = run_with_status
|
|
3065
|
+
self.blackboard = agent.session.blackboard
|
|
2953
3066
|
self.handlers: dict[str, Callable[[str], str]] = {
|
|
2954
3067
|
"/help": self._help,
|
|
2955
3068
|
"/status": self._status,
|
|
@@ -2959,6 +3072,7 @@ class CommandDispatcher:
|
|
|
2959
3072
|
"/reason": self._reason,
|
|
2960
3073
|
"/reason_effort": self._reason_effort,
|
|
2961
3074
|
"/yolo": self._yolo,
|
|
3075
|
+
"/blackboard": self._blackboard,
|
|
2962
3076
|
}
|
|
2963
3077
|
|
|
2964
3078
|
def dispatch(self, user_input: str) -> CommandResult:
|
|
@@ -3023,6 +3137,7 @@ class CommandDispatcher:
|
|
|
3023
3137
|
"yolo: " + yolo,
|
|
3024
3138
|
"conversation: " + str(len(session.conversation)) + "/" + str(session.compact_at),
|
|
3025
3139
|
"tokens: last=" + _format_count(session.last_total_tokens) + " session=" + _format_count(session.session_total_tokens),
|
|
3140
|
+
"blackboard: " + str(len(session.blackboard)) + " items",
|
|
3026
3141
|
"goal: " + (session.current.goal or "(empty)"),
|
|
3027
3142
|
"verification: " + session.current.verification.status,
|
|
3028
3143
|
]
|
|
@@ -3094,6 +3209,17 @@ class CommandDispatcher:
|
|
|
3094
3209
|
return "YOLO is " + ("on" if self.agent.session.yolo else "off")
|
|
3095
3210
|
return "Usage: /yolo [on|off|status]"
|
|
3096
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
|
+
|
|
3097
3223
|
|
|
3098
3224
|
def _format_count(value: int) -> str:
|
|
3099
3225
|
if value <= 0:
|
|
@@ -3201,7 +3327,8 @@ class StatusBar:
|
|
|
3201
3327
|
yolo = " | yolo" if session.yolo else ""
|
|
3202
3328
|
context = str(len(session.conversation)) + "/" + str(session.compact_at)
|
|
3203
3329
|
tokens = "last:" + self._format_count(session.last_total_tokens) + " session:" + self._format_count(session.session_total_tokens)
|
|
3204
|
-
|
|
3330
|
+
blackboard = "bb:" + str(len(session.blackboard))
|
|
3331
|
+
parts = [model + " (" + reasoning + ")" + yolo, "ctx:" + context, "tok:" + tokens, blackboard]
|
|
3205
3332
|
if show_elapsed:
|
|
3206
3333
|
parts.append(f"{turn_elapsed:.1f}s")
|
|
3207
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
|