feat(agent): add tool call cycle detection to prevent infinite loops#2271
Open
flobo3 wants to merge 42 commits intoHKUDS:nightlyfrom
Open
feat(agent): add tool call cycle detection to prevent infinite loops#2271flobo3 wants to merge 42 commits intoHKUDS:nightlyfrom
flobo3 wants to merge 42 commits intoHKUDS:nightlyfrom
Conversation
This was referenced Mar 20, 2026
Register Mistral as a first-class provider with LiteLLM routing, MISTRAL_API_KEY env var, and https://api.mistral.ai/v1 default base. Includes schema field, registry entry, and tests.
Implement file upload and sending for QQ C2C messages Reference: https://github.com/tencent-connect/botpy/blob/master/examples/demo_c2c_reply_file.py --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: chengyongru <chengyongru.ai@gmail.com>
- Fix _read_media_bytes treating local paths as URLs: local file handling code was dead code placed after an early return inside the HTTP try/except block. Restructure to check for local paths (plain path or file:// URI) before URL validation, so files like /home/.../.nanobot/workspace/generated_image.svg can be read and sent correctly. - Add .svg to _IMAGE_EXTS so SVG files are uploaded as file_type=1 (image) instead of file_type=4 (file). - Add tests for local path, file:// URI, and missing file cases. Fixes: HKUDS#1667 (comment) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: guanka001 <guanka001@ke.com>
add OpenVINO Model Server provider
Tool calls from a single LLM response are independent by design — the model batches them precisely because they can run concurrently. Replace the serial for-loop with asyncio.gather so N tools complete in max(time_i) instead of sum(time_i). Made-with: Cursor
Without this flag, a BaseException (e.g. CancelledError from /stop) in one tool would propagate immediately and discard results from the other concurrent tools, corrupting the OpenAI message format. With return_exceptions=True, all tool results are collected; any exception is converted to an error string for the LLM. Made-with: Cursor
The tz parameter was previously only allowed with cron_expr. When users specified tz with at for one-time tasks, it returned an error. Now tz works with both cron_expr and at — naive ISO datetimes are interpreted in the given timezone via ZoneInfo. - Relax validation: allow tz with cron_expr or at - Apply ZoneInfo to naive datetimes in the at branch - Update SKILL.md with at+tz examples - Add automated tests for tz+at combinations Co-authored-by: weitongtong <tongtong.wei@nodeskai.com> Made-with: Cursor
- Restore enable attribute to ExecToolConfig - Remove deprecated memory_window field (was removed in f44c4f9 but brought back by cherry-pick) - Restore exclude=True on openai_codex and github_copilot oauth providers
…conflict resolution The build_messages() method was missing the current_role parameter that loop.py calls with, causing a TypeError at runtime. This restores the parameter with its default value of "user" to match the original PR HKUDS#2104.
Add a new WeChat (微信) channel that connects to personal WeChat using the ilinkai.weixin.qq.com HTTP long-poll API. Protocol reverse-engineered from @tencent-weixin/openclaw-weixin v1.0.2. Features: - QR code login flow (nanobot weixin login) - HTTP long-poll message receiving (getupdates) - Text message sending with proper WeixinMessage format - Media download with AES-128-ECB decryption (image/voice/file/video) - Voice-to-text from WeChat + Groq Whisper fallback - Quoted message (ref_msg) support - Session expiry detection and auto-pause - Server-suggested poll timeout adaptation - Context token caching for replies - Auto-discovery via channel registry No WebSocket, no Node.js bridge, no local WeChat client needed — pure HTTP with a bot token obtained via QR code scan. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cherry-picked from PR HKUDS#2355 (ad128a7) — only agent/context.py and agent/tools/message.py. Co-Authored-By: qulllee <qullkui@tencent.com>
During testing, we discovered that when a user requests the agent to send a file (e.g., "send me IMG_1115.png"), the agent would call read_file to view the content and then reply with text claiming "file sent" — but never actually deliver the file to the user. Root cause: The system prompt stated "Reply directly with text for conversations. Only use the 'message' tool to send to a specific chat channel", which led the LLM to believe text replies were sufficient for all responses, including file delivery. Fix: Add an explicit IMPORTANT instruction in the system prompt telling the LLM it MUST use the 'message' tool with the 'media' parameter to send files, and that read_file only reads content for its own analysis. Co-Authored-By: qulllee <qullkui@tencent.com>
Previously the WeChat channel's send() method only handled text messages,
completely ignoring msg.media. When the agent called message(media=[...]),
the file was never delivered to the user.
Implement the full WeChat CDN upload protocol following the reference
@tencent-weixin/openclaw-weixin v1.0.2:
1. Generate a client-side AES-128 key (16 random bytes)
2. Call getuploadurl with file metadata + hex-encoded AES key
3. AES-128-ECB encrypt the file and POST to CDN with filekey param
4. Read x-encrypted-param from CDN response header as download param
5. Send message with the media item (image/video/file) referencing
the CDN upload
Also adds:
- _encrypt_aes_ecb() for AES-128-ECB encryption (reverse of existing
_decrypt_aes_ecb)
- Media type detection from file extension (image/video/file)
- Graceful error handling: failed media sends notify the user via text
without blocking subsequent text delivery
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ands Move channel-specific login logic from CLI into each channel class via a new `login(force=False)` method on BaseChannel. The `channels login <name>` command now dynamically loads the channel and calls its login() method. - WeixinChannel.login(): calls existing _qr_login(), with force to clear saved token - WhatsAppChannel.login(): sets up bridge and spawns npm process for QR login - CLI no longer contains duplicate login logic per channel - Update CHANNEL_PLUGIN_GUIDE to document the login() hook Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve telegram.py conflict by keeping main's streaming implementation (_StreamBuf + send_delta with edit_message_text approach) over nightly's _send_with_streaming (draft-based approach).
urlparse on Windows puts the path in netloc, not path. Use (parsed.path or parsed.netloc) to get the correct raw path.
* feat: add contextBudgetTokens config field for tool-loop trimming * feat: implement _trim_history_for_budget for tool-loop cost reduction * feat: thread contextBudgetTokens into AgentLoop constructor * feat: wire context budget trimming into agent loop * refactor: move trim_history_for_budget to helpers and add docs - Extract trim_history_for_budget() as a pure function in helpers.py - AgentLoop._trim_history_for_budget becomes a thin wrapper - Add docs/CONTEXT_BUDGET.md with usage guide and trade-off notes - Replace wrapper tests with direct helper unit tests --------- Co-authored-by: chengyongru <chengyongru.ai@gmail.com>
* feat(feishu): add streaming support via CardKit PATCH API Implement send_delta() for Feishu channel using interactive card progressive editing: - First delta creates a card with markdown content and typing cursor - Subsequent deltas throttled at 0.5s to respect 5 QPS PATCH limit - stream_end finalizes with full formatted card (tables, rich markdown) Also refactors _send_message_sync to return message_id (str | None) and adds _patch_card_sync for card updates. Includes 17 new unit tests covering streaming lifecycle, config, card building, and edge cases. Made-with: Cursor * feat(feishu): close CardKit streaming_mode on stream end Call cardkit card.settings after final content update so chat preview leaves default [生成中...] summary (Feishu streaming docs). Made-with: Cursor * style: polish Feishu streaming (PEP8 spacing, drop unused test imports) Made-with: Cursor * docs(feishu): document cardkit:card:write for streaming - README: permissions, upgrade note for existing apps, streaming toggle - CHANNEL_PLUGIN_GUIDE: Feishu CardKit scope and when to disable streaming Made-with: Cursor * docs: address PR 2382 review (test path, plugin guide, README, English docstrings) - Move Feishu streaming tests to tests/channels/ - Remove Feishu CardKit scope from CHANNEL_PLUGIN_GUIDE (plugin-dev doc only) - README Feishu permissions: consistent English - feishu.py: replace Chinese in streaming docstrings/comments Made-with: Cursor
- Add send_max_retries config option (default: 3, range: 0-10) - Implement _send_with_retry in ChannelManager with 1s/2s/4s backoff - Propagate CancelledError for graceful shutdown - Fix telegram send_delta to raise exceptions for Manager retry - Add comprehensive tests for retry logic - Document channel settings in README
Make channel delivery failures raise consistently so retry policy lives in ChannelManager rather than being split across individual channels. Tighten Telegram stream finalization, clarify sendMaxRetries semantics, and align the docs with the behavior the system actually guarantees.
When LLM generates faster than channel can process, asyncio.Queue accumulates multiple _stream_delta messages. Each delta triggers a separate API call (~700ms each), causing visible delay after LLM finishes. Solution: In _dispatch_outbound, drain all queued deltas for the same (channel, chat_id) before sending, combining them into a single API call. Non-matching messages are preserved in a pending buffer for subsequent processing. This reduces N API calls to 1 when queue has N accumulated deltas.
Co-authored-by: Pares Mathur <paresh.2047@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* Added streaming message support with incremental updates for Matrix channel * Improve Matrix message handling and add tests * Adjust Matrix streaming edit interval to 2 seconds --------- Co-authored-by: natan <natan@podbielski>
…clusive (HKUDS#2491) * fix(providers): make max_tokens and max_completion_tokens mutually exclusive * docs: document supports_max_completion_tokens ProviderSpec option
…jection (HKUDS#2488) * feat(command): add /skill slash command for user-activated skill injection * test(command): add tests for /skill slash command * refactor(command): switch skill activation from /skill prefix to $-reference interceptor
Add CycleDetector class that detects when the agent is stuck calling the same tools repeatedly. Two detection strategies: - Same call repetition: identical (tool_name, args) called > N times - Pattern repetition: sequence of calls repeats 2+ times Changes: - New CycleDetector class in nanobot/agent/cycle_detector.py - CycleDetectionConfig in config/schema.py with tunable parameters - Integration into AgentLoop._run_agent_loop() and SubagentManager - Loop prevention hints added to system prompt in context.py - Pass cycle_detection config through CLI commands When a cycle is detected, the agent breaks out of the loop with an informative message, allowing it to try a different approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7885258 to
60e65dd
Compare
5915287 to
5479a44
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
CycleDetectorclass that detects when the agent is stuck in a loop calling the same tools repeatedly. This prevents wasted tokens and time when an LLM gets into a repetitive pattern.Two detection strategies:
(tool_name, args)called more than N times (default: 3)Changes
nanobot/agent/cycle_detector.py— CycleDetector class with configurable parametersCycleDetectionConfiginnanobot/config/schema.pywith tunable settings:enabled: bool = Truewindow_size: int = 20— number of recent calls to trackmax_same_calls: int = 3— max repetitions before triggeringpattern_min_length: int = 2— minimum pattern length to detectpattern_min_repeats: int = 2— minimum pattern repetitionsnanobot/agent/loop.py— integration into_run_agent_loop()nanobot/agent/subagent.py— integration into subagent executionnanobot/agent/context.py— added "Loop Prevention" hints in system promptnanobot/cli/commands.py— pass config through CLIBehavior
When a cycle is detected:
Example
Configuration
Users can tune detection via
nanobot.yaml:Test Plan
python -c "import nanobot.agent.cycle_detector"🤖 Generated with Claude Code