refactor: enforce BasedPyright strict type checking (#5158)

This commit is contained in:
chengyongru
2026-07-29 21:37:11 +08:00
committed by GitHub
parent e703481755
commit 757ad9c764
166 changed files with 4728 additions and 2621 deletions
+48 -39
View File
@@ -9,16 +9,20 @@ import sys
import time
from contextlib import suppress
from dataclasses import dataclass
from typing import Literal
from typing import TYPE_CHECKING, Any, Literal, cast
from nanobot import __version__
from nanobot.agent.goal_permission import goal_mutation_permission
from nanobot.bus.events import OutboundMessage
from nanobot.command.router import CommandContext, CommandRouter, normalize_command_text
from nanobot.utils.helpers import build_status_content
from nanobot.utils.restart import set_restart_notice_to_env
from nanobot.utils.workspace_prompts import initialize_workspace_prompt
if TYPE_CHECKING:
from nanobot.agent.loop import AgentLoop
from nanobot.session.manager import Session
from nanobot.utils.gitstore import CommitInfo
# WebUI protocol contract for how a slash command participates in turn state:
# - side_channel: returns control text without starting or ending an agent turn.
# - finalize_active_turn: side-channel command that also closes the active UI turn.
@@ -199,9 +203,9 @@ async def cmd_stop(ctx: CommandContext) -> OutboundMessage:
"""Cancel all active tasks and subagents for the session."""
loop = ctx.loop
msg = ctx.msg
total = await loop._cancel_active_tasks(ctx.key)
total = await loop._cancel_active_tasks(ctx.key) # pyright: ignore[reportPrivateUsage]
# Also drain pending queue to prevent mid-turn injection deadlock
pending = loop._pending_queues.pop(ctx.key, None)
pending = loop._pending_queues.pop(ctx.key, None) # pyright: ignore[reportPrivateUsage]
if pending is not None:
while not pending.empty():
try:
@@ -228,14 +232,14 @@ async def cmd_restart(ctx: CommandContext) -> OutboundMessage:
async def _do_restart():
await asyncio.sleep(1)
argv = [sys.executable, "-m", "nanobot"] + sys.argv[1:]
mode = getattr(ctx.loop, "restart_mode", "auto") or "auto"
mode = ctx.loop.restart_mode or "auto"
if mode == "auto":
mode = "spawn" if sys.platform == "win32" else "exec"
if mode == "exec":
os.execv(sys.executable, argv)
return
if mode == "spawn":
kwargs = {}
kwargs: dict[str, Any] = {}
if sys.platform == "win32":
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
subprocess.Popen(argv, **kwargs)
@@ -260,21 +264,20 @@ async def cmd_status(ctx: CommandContext) -> OutboundMessage:
runtime=runtime,
)
if ctx_est <= 0:
ctx_est = loop._last_usage.get("prompt_tokens", 0)
ctx_est = loop._last_usage.get("prompt_tokens", 0) # pyright: ignore[reportPrivateUsage]
# Fetch web search provider usage (best-effort, never blocks the response)
search_usage_text: str | None = None
# Never let usage fetch break /status
with suppress(Exception):
from nanobot.utils.searchusage import fetch_search_usage
web_cfg = getattr(loop, "web_config", None)
search_cfg = getattr(web_cfg, "search", None) if web_cfg else None
if search_cfg is not None:
provider = getattr(search_cfg, "provider", "duckduckgo")
api_key = getattr(search_cfg, "api_key", "") or None
usage = await fetch_search_usage(provider=provider, api_key=api_key)
search_usage_text = usage.format()
active_tasks = loop._active_tasks.get(ctx.key, [])
search_cfg = loop.web_config.search
usage = await fetch_search_usage(
provider=search_cfg.provider,
api_key=search_cfg.api_key or None,
)
search_usage_text = usage.format()
active_tasks = loop._active_tasks.get(ctx.key, []) # pyright: ignore[reportPrivateUsage]
task_count = sum(1 for t in active_tasks if not t.done())
with suppress(Exception):
task_count += loop.subagents.get_running_count_by_session(ctx.key)
@@ -283,7 +286,7 @@ async def cmd_status(ctx: CommandContext) -> OutboundMessage:
chat_id=ctx.msg.chat_id,
content=build_status_content(
version=__version__, model=runtime.model,
start_time=loop._start_time, last_usage=loop._last_usage,
start_time=loop._start_time, last_usage=loop._last_usage, # pyright: ignore[reportPrivateUsage]
context_window_tokens=runtime.context_window_tokens,
session_msg_count=len(session.get_history(max_messages=0)),
context_tokens_estimate=ctx_est,
@@ -298,17 +301,18 @@ async def cmd_status(ctx: CommandContext) -> OutboundMessage:
async def cmd_new(ctx: CommandContext) -> OutboundMessage:
"""Stop active task and start a fresh session."""
loop = ctx.loop
await loop._cancel_active_tasks(ctx.key)
await loop._cancel_active_tasks(ctx.key) # pyright: ignore[reportPrivateUsage]
session = ctx.session or loop.sessions.get_or_create(ctx.key)
snapshot = session.messages[session.last_consolidated:]
runtime = None
if snapshot:
runtime = ctx.runtime or loop.runtime_for_session(session)
session.clear()
loop.sessions.save(session)
loop.sessions.invalidate(session.key)
if snapshot:
loop._schedule_background(
loop.consolidator.archive(
if snapshot and runtime is not None:
loop._schedule_background( # pyright: ignore[reportPrivateUsage]
loop.consolidator.archive( # pyright: ignore[reportUnknownMemberType]
snapshot,
runtime=runtime,
session_key=ctx.key,
@@ -325,7 +329,7 @@ def _format_preset_names(names: list[str]) -> str:
return ", ".join(f"`{name}`" for name in names) if names else "(none configured)"
def _model_preset_names(loop) -> list[str]:
def _model_preset_names(loop: AgentLoop) -> list[str]:
names = set(loop.model_presets)
names.add("default")
return ["default", *sorted(name for name in names if name != "default")]
@@ -335,7 +339,7 @@ def _command_error_message(exc: Exception) -> str:
return str(exc.args[0]) if isinstance(exc, KeyError) and exc.args else str(exc)
def _model_command_status(loop, session) -> str:
def _model_command_status(loop: AgentLoop, session: Session) -> str:
names = _model_preset_names(loop)
try:
runtime = loop.runtime_for_session(session, recover_removed=False)
@@ -401,8 +405,7 @@ async def cmd_model(ctx: CommandContext) -> OutboundMessage:
f"- Model: `{runtime.model}`",
f"- Context window: {runtime.context_window_tokens}",
]
if max_tokens is not None:
lines.append(f"- Max output tokens: {max_tokens}")
lines.append(f"- Max output tokens: {max_tokens}")
return OutboundMessage(
channel=ctx.msg.channel,
chat_id=ctx.msg.chat_id,
@@ -442,8 +445,7 @@ async def cmd_dream(ctx: CommandContext) -> OutboundMessage:
return
prompt, last_cursor = result
key = dream_session_key()
resolve_dream_runtime = getattr(loop, "dream_runtime", None)
dream_runtime = resolve_dream_runtime() if callable(resolve_dream_runtime) else None
dream_runtime = loop.dream_runtime()
resp = await loop.process_direct(
prompt,
session_key=key,
@@ -640,7 +642,12 @@ def _format_changed_files(diff: str) -> str:
_DREAM_COMMIT_PREFIX = "dream:"
def _format_dream_log_content(commit, diff: str, *, requested_sha: str | None = None) -> str:
def _format_dream_log_content(
commit: CommitInfo,
diff: str,
*,
requested_sha: str | None = None,
) -> str:
files_line = _format_changed_files(diff)
lines = [
"## Dream Update",
@@ -668,7 +675,7 @@ def _format_dream_log_content(commit, diff: str, *, requested_sha: str | None =
return "\n".join(lines)
def _format_dream_restore_list(commits: list) -> str:
def _format_dream_restore_list(commits: list[CommitInfo]) -> str:
lines = [
"## Dream Restore",
"",
@@ -806,14 +813,20 @@ _HISTORY_MAX_COUNT = 50
_HISTORY_MAX_CONTENT_CHARS = 200
def _format_history_message(msg: dict) -> str | None:
def _format_history_message(msg: dict[str, Any]) -> str | None:
"""Format a single history message for display. Returns None to skip."""
role = msg.get("role")
if role not in ("user", "assistant"):
return None
content = msg.get("content") or ""
if isinstance(content, list):
parts = [b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text"]
parts = [
text
for block in cast(list[object], content)
if (item := cast(dict[str, Any], block) if isinstance(block, dict) else None)
and item.get("type") == "text"
and isinstance(text := item.get("text"), str)
]
content = " ".join(parts)
content = str(content).strip()
if not content:
@@ -863,6 +876,8 @@ async def cmd_history(ctx: CommandContext) -> OutboundMessage:
async def cmd_goal(ctx: CommandContext) -> OutboundMessage | None:
"""Mark this turn as an explicit sustained-goal request."""
from nanobot.agent.goal_permission import goal_mutation_permission
goal = ctx.args.strip()
if not goal:
return OutboundMessage(
@@ -923,7 +938,7 @@ async def cmd_skill(ctx: CommandContext) -> OutboundMessage:
else:
lines = [f"Available skills ({len(skills)}):", ""]
for entry in skills:
desc = loop.context.skills._get_skill_description(entry["name"])
desc = loop.context.skills.get_skill_description(entry["name"])
lines.append(f"- **{entry['name']}** — {desc}")
content = "\n".join(lines)
return OutboundMessage(
@@ -951,15 +966,9 @@ async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
from nanobot.triggers.local_store import LocalTriggerStore
loop = ctx.loop
workspace = getattr(loop, "workspace", None)
if workspace is None:
workspace = getattr(getattr(loop, "context", None), "workspace", None)
if workspace is None:
raise RuntimeError("workspace unavailable for trigger creation")
store = getattr(loop, "local_trigger_store", None)
store = loop.local_trigger_store
if store is None:
store = LocalTriggerStore(workspace)
store = LocalTriggerStore(loop.workspace)
from nanobot.session.keys import UNIFIED_SESSION_KEY