diff --git a/README.md b/README.md index ea606de..fed25c8 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ⚡️ Delivers core agent functionality in just **~4,000** lines of code — **99% smaller** than Clawdbot's 430k+ lines. -📏 Real-time line count: **3,578 lines** (run `bash core_agent_lines.sh` to verify anytime) +📏 Real-time line count: **3,510 lines** (run `bash core_agent_lines.sh` to verify anytime) ## 📢 News diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index b9c0790..d807854 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -73,9 +73,7 @@ Skills with available="false" need dependencies installed first - you can try in def _get_identity(self) -> str: """Get the core identity section.""" from datetime import datetime - import time as _time now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)") - tz = _time.strftime("%Z") or "UTC" workspace_path = str(self.workspace.expanduser().resolve()) system = platform.system() runtime = f"{'macOS' if system == 'Darwin' else system} {platform.machine()}, Python {platform.python_version()}" @@ -90,7 +88,7 @@ You are nanobot, a helpful AI assistant. You have access to tools that allow you - Spawn subagents for complex background tasks ## Current Time -{now} ({tz}) +{now} ## Runtime {runtime} @@ -105,7 +103,7 @@ IMPORTANT: When responding to direct questions or conversations, reply directly Only use the 'message' tool when you need to send a message to a specific chat channel (like WhatsApp). For normal conversation, just respond with text - do not call the message tool. -Always be helpful, accurate, and concise. When using tools, think step by step: what you know, what you need, and why you chose this tool. +Always be helpful, accurate, and concise. When using tools, explain what you're doing. When remembering something, write to {workspace_path}/memory/MEMORY.md""" def _load_bootstrap_files(self) -> str: diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index a3ab678..b15803a 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -250,8 +250,6 @@ class AgentLoop: messages = self.context.add_tool_result( messages, tool_call.id, tool_call.name, result ) - # Interleaved CoT: reflect before next action - messages.append({"role": "user", "content": "Reflect on the results and decide next steps."}) else: # No tool calls, we're done final_content = response.content @@ -357,8 +355,6 @@ class AgentLoop: messages = self.context.add_tool_result( messages, tool_call.id, tool_call.name, result ) - # Interleaved CoT: reflect before next action - messages.append({"role": "user", "content": "Reflect on the results and decide next steps."}) else: final_content = response.content break diff --git a/nanobot/agent/subagent.py b/nanobot/agent/subagent.py index 9e0cd7c..6113efb 100644 --- a/nanobot/agent/subagent.py +++ b/nanobot/agent/subagent.py @@ -12,7 +12,7 @@ from nanobot.bus.events import InboundMessage from nanobot.bus.queue import MessageBus from nanobot.providers.base import LLMProvider from nanobot.agent.tools.registry import ToolRegistry -from nanobot.agent.tools.filesystem import ReadFileTool, WriteFileTool, EditFileTool, ListDirTool +from nanobot.agent.tools.filesystem import ReadFileTool, WriteFileTool, ListDirTool from nanobot.agent.tools.shell import ExecTool from nanobot.agent.tools.web import WebSearchTool, WebFetchTool @@ -101,7 +101,6 @@ class SubagentManager: allowed_dir = self.workspace if self.restrict_to_workspace else None tools.register(ReadFileTool(allowed_dir=allowed_dir)) tools.register(WriteFileTool(allowed_dir=allowed_dir)) - tools.register(EditFileTool(allowed_dir=allowed_dir)) tools.register(ListDirTool(allowed_dir=allowed_dir)) tools.register(ExecTool( working_dir=str(self.workspace), @@ -211,18 +210,13 @@ Summarize this naturally for the user. Keep it brief (1-2 sentences). Do not men def _build_subagent_prompt(self, task: str) -> str: """Build a focused system prompt for the subagent.""" - from datetime import datetime - import time as _time - now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)") - tz = _time.strftime("%Z") or "UTC" - return f"""# Subagent -## Current Time -{now} ({tz}) - You are a subagent spawned by the main agent to complete a specific task. +## Your Task +{task} + ## Rules 1. Stay focused - complete only the assigned task, nothing else 2. Your final response will be reported back to the main agent @@ -242,7 +236,6 @@ You are a subagent spawned by the main agent to complete a specific task. ## Workspace Your workspace is at: {self.workspace} -Skills are available at: {self.workspace}/skills/ (read SKILL.md files as needed) When you have completed the task, provide a clear summary of your findings or actions.""" diff --git a/nanobot/agent/tools/cron.py b/nanobot/agent/tools/cron.py index 9f1ecdb..ec0d2cd 100644 --- a/nanobot/agent/tools/cron.py +++ b/nanobot/agent/tools/cron.py @@ -50,10 +50,6 @@ class CronTool(Tool): "type": "string", "description": "Cron expression like '0 9 * * *' (for scheduled tasks)" }, - "at": { - "type": "string", - "description": "ISO datetime for one-time execution (e.g. '2026-02-12T10:30:00')" - }, "job_id": { "type": "string", "description": "Job ID (for remove)" @@ -68,38 +64,30 @@ class CronTool(Tool): message: str = "", every_seconds: int | None = None, cron_expr: str | None = None, - at: str | None = None, job_id: str | None = None, **kwargs: Any ) -> str: if action == "add": - return self._add_job(message, every_seconds, cron_expr, at) + return self._add_job(message, every_seconds, cron_expr) elif action == "list": return self._list_jobs() elif action == "remove": return self._remove_job(job_id) return f"Unknown action: {action}" - def _add_job(self, message: str, every_seconds: int | None, cron_expr: str | None, at: str | None) -> str: + def _add_job(self, message: str, every_seconds: int | None, cron_expr: str | None) -> str: if not message: return "Error: message is required for add" if not self._channel or not self._chat_id: return "Error: no session context (channel/chat_id)" # Build schedule - delete_after = False if every_seconds: schedule = CronSchedule(kind="every", every_ms=every_seconds * 1000) elif cron_expr: schedule = CronSchedule(kind="cron", expr=cron_expr) - elif at: - from datetime import datetime - dt = datetime.fromisoformat(at) - at_ms = int(dt.timestamp() * 1000) - schedule = CronSchedule(kind="at", at_ms=at_ms) - delete_after = True else: - return "Error: either every_seconds, cron_expr, or at is required" + return "Error: either every_seconds or cron_expr is required" job = self._cron.add_job( name=message[:30], @@ -108,7 +96,6 @@ class CronTool(Tool): deliver=True, channel=self._channel, to=self._chat_id, - delete_after_run=delete_after, ) return f"Created job '{job.name}' (id: {job.id})" diff --git a/nanobot/channels/dingtalk.py b/nanobot/channels/dingtalk.py index 4a8cdd9..72d3afd 100644 --- a/nanobot/channels/dingtalk.py +++ b/nanobot/channels/dingtalk.py @@ -137,15 +137,8 @@ class DingTalkChannel(BaseChannel): logger.info("DingTalk bot started with Stream Mode") - # Reconnect loop: restart stream if SDK exits or crashes - while self._running: - try: - await self._client.start() - except Exception as e: - logger.warning(f"DingTalk stream error: {e}") - if self._running: - logger.info("Reconnecting DingTalk stream in 5 seconds...") - await asyncio.sleep(5) + # client.start() is an async infinite loop handling the websocket connection + await self._client.start() except Exception as e: logger.exception(f"Failed to start DingTalk channel: {e}") diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index 23d1415..1c176a2 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -98,15 +98,12 @@ class FeishuChannel(BaseChannel): log_level=lark.LogLevel.INFO ) - # Start WebSocket client in a separate thread with reconnect loop + # Start WebSocket client in a separate thread def run_ws(): - while self._running: - try: - self._ws_client.start() - except Exception as e: - logger.warning(f"Feishu WebSocket error: {e}") - if self._running: - import time; time.sleep(5) + try: + self._ws_client.start() + except Exception as e: + logger.error(f"Feishu WebSocket error: {e}") self._ws_thread = threading.Thread(target=run_ws, daemon=True) self._ws_thread.start() diff --git a/nanobot/channels/qq.py b/nanobot/channels/qq.py index 0e8fe66..5964d30 100644 --- a/nanobot/channels/qq.py +++ b/nanobot/channels/qq.py @@ -75,15 +75,12 @@ class QQChannel(BaseChannel): logger.info("QQ bot started (C2C private message)") async def _run_bot(self) -> None: - """Run the bot connection with auto-reconnect.""" - while self._running: - try: - await self._client.start(appid=self.config.app_id, secret=self.config.secret) - except Exception as e: - logger.warning(f"QQ bot error: {e}") - if self._running: - logger.info("Reconnecting QQ bot in 5 seconds...") - await asyncio.sleep(5) + """Run the bot connection.""" + try: + await self._client.start(appid=self.config.app_id, secret=self.config.secret) + except Exception as e: + logger.error(f"QQ auth failed, check AppID/Secret at q.qq.com: {e}") + self._running = False async def stop(self) -> None: """Stop the QQ bot.""" diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index 1abd600..ff46c86 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING from loguru import logger from telegram import BotCommand, Update from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes -from telegram.request import HTTPXRequest from nanobot.bus.events import OutboundMessage from nanobot.bus.queue import MessageBus @@ -122,13 +121,11 @@ class TelegramChannel(BaseChannel): self._running = True - # Build the application with larger connection pool to avoid pool-timeout on long runs - req = HTTPXRequest(connection_pool_size=16, pool_timeout=5.0, connect_timeout=30.0, read_timeout=30.0) - builder = Application.builder().token(self.config.token).request(req).get_updates_request(req) + # Build the application + builder = Application.builder().token(self.config.token) if self.config.proxy: builder = builder.proxy(self.config.proxy).get_updates_proxy(self.config.proxy) self._app = builder.build() - self._app.add_error_handler(self._on_error) # Add command handlers self._app.add_handler(CommandHandler("start", self._on_start)) @@ -389,10 +386,6 @@ class TelegramChannel(BaseChannel): except Exception as e: logger.debug(f"Typing indicator stopped for {chat_id}: {e}") - async def _on_error(self, update: object, context: ContextTypes.DEFAULT_TYPE) -> None: - """Log polling / handler errors instead of silently swallowing them.""" - logger.error(f"Telegram error: {context.error}") - def _get_extension(self, media_type: str, mime_type: str | None) -> str: """Get file extension based on media type.""" if mime_type: diff --git a/nanobot/skills/cron/SKILL.md b/nanobot/skills/cron/SKILL.md index 7db25d8..c8beecb 100644 --- a/nanobot/skills/cron/SKILL.md +++ b/nanobot/skills/cron/SKILL.md @@ -7,11 +7,10 @@ description: Schedule reminders and recurring tasks. Use the `cron` tool to schedule reminders or recurring tasks. -## Three Modes +## Two Modes 1. **Reminder** - message is sent directly to user 2. **Task** - message is a task description, agent executes and sends result -3. **One-time** - runs once at a specific time, then auto-deletes ## Examples @@ -25,11 +24,6 @@ Dynamic task (agent executes each time): cron(action="add", message="Check HKUDS/nanobot GitHub stars and report", every_seconds=600) ``` -One-time scheduled task (compute ISO datetime from current time): -``` -cron(action="add", message="Remind me about the meeting", at="") -``` - List/remove: ``` cron(action="list") @@ -44,4 +38,3 @@ cron(action="remove", job_id="abc123") | every hour | every_seconds: 3600 | | every day at 8am | cron_expr: "0 8 * * *" | | weekdays at 5pm | cron_expr: "0 17 * * 1-5" | -| at a specific time | at: ISO datetime string (compute from current time) |