From 0c412b3728a430f3fed7daea753a23962ad84fa1 Mon Sep 17 00:00:00 2001 From: Yingwen Luo-LUOYW Date: Sun, 22 Feb 2026 23:13:09 +0800 Subject: [PATCH 1/3] feat(channels): add send_progress option to control progress message delivery Add a boolean config option `channels.sendProgress` (default: false) to control whether progress messages (marked with `_progress` metadata) are sent to chat channels. When disabled, progress messages are filtered out in the outbound dispatcher. --- nanobot/channels/manager.py | 3 +++ nanobot/config/schema.py | 1 + 2 files changed, 4 insertions(+) diff --git a/nanobot/channels/manager.py b/nanobot/channels/manager.py index 6fbab04..8a03883 100644 --- a/nanobot/channels/manager.py +++ b/nanobot/channels/manager.py @@ -193,6 +193,9 @@ class ChannelManager: timeout=1.0 ) + if msg.metadata.get("_progress") and not self.config.channels.send_progress: + continue + channel = self.channels.get(msg.channel) if channel: try: diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 966d11d..bd602dc 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -168,6 +168,7 @@ class QQConfig(Base): class ChannelsConfig(Base): """Configuration for chat channels.""" + send_progress: bool = False whatsapp: WhatsAppConfig = Field(default_factory=WhatsAppConfig) telegram: TelegramConfig = Field(default_factory=TelegramConfig) discord: DiscordConfig = Field(default_factory=DiscordConfig) From bc32e85c25f2366322626d6c8ff98574614be711 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Mon, 23 Feb 2026 05:51:44 +0000 Subject: [PATCH 2/3] fix(memory): trigger consolidation by unconsolidated count, not total --- nanobot/agent/loop.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index b05ba90..0bd05a8 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -352,7 +352,8 @@ class AgentLoop: return OutboundMessage(channel=msg.channel, chat_id=msg.chat_id, content="🐈 nanobot commands:\n/new — Start a new conversation\n/help — Show available commands") - if len(session.messages) > self.memory_window and session.key not in self._consolidating: + unconsolidated = len(session.messages) - session.last_consolidated + if (unconsolidated >= self.memory_window and session.key not in self._consolidating): self._consolidating.add(session.key) lock = self._get_consolidation_lock(session.key) From df2c837e252b76a8d3a91bd8a64b8987089f6892 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Mon, 23 Feb 2026 07:12:41 +0000 Subject: [PATCH 3/3] feat(channels): split send_progress into send_progress + send_tool_hints --- nanobot/agent/loop.py | 12 +++++++----- nanobot/channels/manager.py | 7 +++++-- nanobot/cli/commands.py | 19 +++++++++++++++++-- nanobot/config/schema.py | 3 ++- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 0bd05a8..cd67bdc 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -27,7 +27,7 @@ from nanobot.providers.base import LLMProvider from nanobot.session.manager import Session, SessionManager if TYPE_CHECKING: - from nanobot.config.schema import ExecToolConfig + from nanobot.config.schema import ChannelsConfig, ExecToolConfig from nanobot.cron.service import CronService @@ -59,9 +59,11 @@ class AgentLoop: restrict_to_workspace: bool = False, session_manager: SessionManager | None = None, mcp_servers: dict | None = None, + channels_config: ChannelsConfig | None = None, ): from nanobot.config.schema import ExecToolConfig self.bus = bus + self.channels_config = channels_config self.provider = provider self.workspace = workspace self.model = model or provider.get_default_model() @@ -172,7 +174,7 @@ class AgentLoop: async def _run_agent_loop( self, initial_messages: list[dict], - on_progress: Callable[[str], Awaitable[None]] | None = None, + on_progress: Callable[..., Awaitable[None]] | None = None, ) -> tuple[str | None, list[str]]: """Run the agent iteration loop. Returns (final_content, tools_used).""" messages = initial_messages @@ -196,8 +198,7 @@ class AgentLoop: clean = self._strip_think(response.content) if clean: await on_progress(clean) - else: - await on_progress(self._tool_hint(response.tool_calls)) + await on_progress(self._tool_hint(response.tool_calls), tool_hint=True) tool_call_dicts = [ { @@ -383,9 +384,10 @@ class AgentLoop: channel=msg.channel, chat_id=msg.chat_id, ) - async def _bus_progress(content: str) -> None: + async def _bus_progress(content: str, *, tool_hint: bool = False) -> None: meta = dict(msg.metadata or {}) meta["_progress"] = True + meta["_tool_hint"] = tool_hint await self.bus.publish_outbound(OutboundMessage( channel=msg.channel, chat_id=msg.chat_id, content=content, metadata=meta, )) diff --git a/nanobot/channels/manager.py b/nanobot/channels/manager.py index 8a03883..77b7294 100644 --- a/nanobot/channels/manager.py +++ b/nanobot/channels/manager.py @@ -193,8 +193,11 @@ class ChannelManager: timeout=1.0 ) - if msg.metadata.get("_progress") and not self.config.channels.send_progress: - continue + if msg.metadata.get("_progress"): + if msg.metadata.get("_tool_hint") and not self.config.channels.send_tool_hints: + continue + if not msg.metadata.get("_tool_hint") and not self.config.channels.send_progress: + continue channel = self.channels.get(msg.channel) if channel: diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index f1f9b30..fcbd370 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -368,6 +368,7 @@ def gateway( restrict_to_workspace=config.tools.restrict_to_workspace, session_manager=session_manager, mcp_servers=config.tools.mcp_servers, + channels_config=config.channels, ) # Set cron callback (needs agent) @@ -484,6 +485,7 @@ def agent( cron_service=cron, restrict_to_workspace=config.tools.restrict_to_workspace, mcp_servers=config.tools.mcp_servers, + channels_config=config.channels, ) # Show spinner when logs are off (no output to miss); skip when logs are on @@ -494,7 +496,12 @@ def agent( # Animated spinner is safe to use with prompt_toolkit input handling return console.status("[dim]nanobot is thinking...[/dim]", spinner="dots") - async def _cli_progress(content: str) -> None: + async def _cli_progress(content: str, *, tool_hint: bool = False) -> None: + ch = agent_loop.channels_config + if ch and tool_hint and not ch.send_tool_hints: + return + if ch and not tool_hint and not ch.send_progress: + return console.print(f" [dim]↳ {content}[/dim]") if message: @@ -535,7 +542,14 @@ def agent( try: msg = await asyncio.wait_for(bus.consume_outbound(), timeout=1.0) if msg.metadata.get("_progress"): - console.print(f" [dim]↳ {msg.content}[/dim]") + is_tool_hint = msg.metadata.get("_tool_hint", False) + ch = agent_loop.channels_config + if ch and is_tool_hint and not ch.send_tool_hints: + pass + elif ch and not is_tool_hint and not ch.send_progress: + pass + else: + console.print(f" [dim]↳ {msg.content}[/dim]") elif not turn_done.is_set(): if msg.content: turn_response.append(msg.content) @@ -961,6 +975,7 @@ def cron_run( exec_config=config.tools.exec, restrict_to_workspace=config.tools.restrict_to_workspace, mcp_servers=config.tools.mcp_servers, + channels_config=config.channels, ) store_path = get_data_dir() / "cron" / "jobs.json" diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index fc9fede..9265602 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -168,7 +168,8 @@ class QQConfig(Base): class ChannelsConfig(Base): """Configuration for chat channels.""" - send_progress: bool = False + send_progress: bool = True # stream agent's text progress to the channel + send_tool_hints: bool = False # stream tool-call hints (e.g. read_file("…")) whatsapp: WhatsAppConfig = Field(default_factory=WhatsAppConfig) telegram: TelegramConfig = Field(default_factory=TelegramConfig) discord: DiscordConfig = Field(default_factory=DiscordConfig)