From d9462284e1549f86570874096e2e4ea343a7bc17 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Mon, 23 Feb 2026 09:13:08 +0000 Subject: [PATCH] improve agent reliability: behavioral constraints, full tool history, error hints --- README.md | 2 +- nanobot/agent/context.py | 18 +++++++----- nanobot/agent/loop.py | 49 +++++++++++++++++++++++---------- nanobot/agent/tools/registry.py | 13 ++++++--- nanobot/config/schema.py | 4 +-- 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 8c47f0f..148c8f4 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,862 lines** (run `bash core_agent_lines.sh` to verify anytime) +📏 Real-time line count: **3,897 lines** (run `bash core_agent_lines.sh` to verify anytime) ## 📢 News diff --git a/nanobot/agent/context.py b/nanobot/agent/context.py index c5869f3..98c13f2 100644 --- a/nanobot/agent/context.py +++ b/nanobot/agent/context.py @@ -96,14 +96,18 @@ Your workspace is at: {workspace_path} - History log: {workspace_path}/memory/HISTORY.md (grep-searchable) - Custom skills: {workspace_path}/skills/{{skill-name}}/SKILL.md -IMPORTANT: When responding to direct questions or conversations, reply directly with your text response. -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. +Reply directly with text for conversations. Only use the 'message' tool to send to a specific chat channel. -Always be helpful, accurate, and concise. Before calling tools, briefly tell the user what you're about to do (one short sentence in the user's language). -If you need to use tools, call them directly — never send a preliminary message like "Let me check" without actually calling a tool. -When remembering something important, write to {workspace_path}/memory/MEMORY.md -To recall past events, grep {workspace_path}/memory/HISTORY.md""" +## Tool Call Guidelines +- Before calling tools, you may briefly state your intent (e.g. "Let me check that"), but NEVER predict or describe the expected result before receiving it. +- Before modifying a file, read it first to confirm its current content. +- Do not assume a file or directory exists — use list_dir or read_file to verify. +- After writing or editing a file, re-read it if accuracy matters. +- If a tool call fails, analyze the error before retrying with a different approach. + +## Memory +- Remember important facts: write to {workspace_path}/memory/MEMORY.md +- Recall past events: grep {workspace_path}/memory/HISTORY.md""" def _load_bootstrap_files(self) -> str: """Load all bootstrap files from workspace.""" diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 296c908..8be8e51 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -49,10 +49,10 @@ class AgentLoop: provider: LLMProvider, workspace: Path, model: str | None = None, - max_iterations: int = 20, + max_iterations: int = 40, temperature: float = 0.1, max_tokens: int = 4096, - memory_window: int = 50, + memory_window: int = 100, brave_api_key: str | None = None, exec_config: ExecToolConfig | None = None, cron_service: CronService | None = None, @@ -175,8 +175,8 @@ class AgentLoop: self, initial_messages: list[dict], on_progress: Callable[..., Awaitable[None]] | None = None, - ) -> tuple[str | None, list[str]]: - """Run the agent iteration loop. Returns (final_content, tools_used).""" + ) -> tuple[str | None, list[str], list[dict]]: + """Run the agent iteration loop. Returns (final_content, tools_used, messages).""" messages = initial_messages iteration = 0 final_content = None @@ -228,7 +228,14 @@ class AgentLoop: final_content = self._strip_think(response.content) break - return final_content, tools_used + if final_content is None and iteration >= self.max_iterations: + logger.warning("Max iterations ({}) reached", self.max_iterations) + final_content = ( + f"I reached the maximum number of tool call iterations ({self.max_iterations}) " + "without completing the task. You can try breaking the task into smaller steps." + ) + + return final_content, tools_used, messages async def run(self) -> None: """Run the agent loop, processing messages from the bus.""" @@ -301,13 +308,13 @@ class AgentLoop: key = f"{channel}:{chat_id}" session = self.sessions.get_or_create(key) self._set_tool_context(channel, chat_id, msg.metadata.get("message_id")) + history = session.get_history(max_messages=self.memory_window) messages = self.context.build_messages( - history=session.get_history(max_messages=self.memory_window), + history=history, current_message=msg.content, channel=channel, chat_id=chat_id, ) - final_content, _ = await self._run_agent_loop(messages) - session.add_message("user", f"[System: {msg.sender_id}] {msg.content}") - session.add_message("assistant", final_content or "Background task completed.") + final_content, _, all_msgs = await self._run_agent_loop(messages) + self._save_turn(session, all_msgs, 1 + len(history)) self.sessions.save(session) return OutboundMessage(channel=channel, chat_id=chat_id, content=final_content or "Background task completed.") @@ -377,8 +384,9 @@ class AgentLoop: if isinstance(message_tool, MessageTool): message_tool.start_turn() + history = session.get_history(max_messages=self.memory_window) initial_messages = self.context.build_messages( - history=session.get_history(max_messages=self.memory_window), + history=history, current_message=msg.content, media=msg.media if msg.media else None, channel=msg.channel, chat_id=msg.chat_id, @@ -392,7 +400,7 @@ class AgentLoop: channel=msg.channel, chat_id=msg.chat_id, content=content, metadata=meta, )) - final_content, tools_used = await self._run_agent_loop( + final_content, _, all_msgs = await self._run_agent_loop( initial_messages, on_progress=on_progress or _bus_progress, ) @@ -402,9 +410,7 @@ class AgentLoop: preview = final_content[:120] + "..." if len(final_content) > 120 else final_content logger.info("Response to {}:{}: {}", msg.channel, msg.sender_id, preview) - session.add_message("user", msg.content) - session.add_message("assistant", final_content, - tools_used=tools_used if tools_used else None) + self._save_turn(session, all_msgs, 1 + len(history)) self.sessions.save(session) if message_tool := self.tools.get("message"): @@ -416,6 +422,21 @@ class AgentLoop: metadata=msg.metadata or {}, ) + _TOOL_RESULT_MAX_CHARS = 500 + + def _save_turn(self, session: Session, messages: list[dict], skip: int) -> None: + """Save new-turn messages into session, truncating large tool results.""" + from datetime import datetime + for m in messages[skip:]: + entry = {k: v for k, v in m.items() if k != "reasoning_content"} + if entry.get("role") == "tool" and isinstance(entry.get("content"), str): + content = entry["content"] + if len(content) > self._TOOL_RESULT_MAX_CHARS: + entry["content"] = content[:self._TOOL_RESULT_MAX_CHARS] + "\n... (truncated)" + entry.setdefault("timestamp", datetime.now().isoformat()) + session.messages.append(entry) + session.updated_at = datetime.now() + async def _consolidate_memory(self, session, archive_all: bool = False) -> bool: """Delegate to MemoryStore.consolidate(). Returns True on success.""" return await MemoryStore(self.workspace).consolidate( diff --git a/nanobot/agent/tools/registry.py b/nanobot/agent/tools/registry.py index d9b33ff..8256a59 100644 --- a/nanobot/agent/tools/registry.py +++ b/nanobot/agent/tools/registry.py @@ -49,17 +49,22 @@ class ToolRegistry: Raises: KeyError: If tool not found. """ + _HINT = "\n\n[Analyze the error above and try a different approach.]" + tool = self._tools.get(name) if not tool: - return f"Error: Tool '{name}' not found" + return f"Error: Tool '{name}' not found. Available: {', '.join(self.tool_names)}" try: errors = tool.validate_params(params) if errors: - return f"Error: Invalid parameters for tool '{name}': " + "; ".join(errors) - return await tool.execute(**params) + return f"Error: Invalid parameters for tool '{name}': " + "; ".join(errors) + _HINT + result = await tool.execute(**params) + if isinstance(result, str) and result.startswith("Error"): + return result + _HINT + return result except Exception as e: - return f"Error executing {name}: {str(e)}" + return f"Error executing {name}: {str(e)}" + _HINT @property def tool_names(self) -> list[str]: diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 10e3fa5..fe8dd83 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -188,8 +188,8 @@ class AgentDefaults(Base): model: str = "anthropic/claude-opus-4-5" max_tokens: int = 8192 temperature: float = 0.1 - max_tool_iterations: int = 20 - memory_window: int = 50 + max_tool_iterations: int = 40 + memory_window: int = 100 class AgentsConfig(Base):