feat(agent): add LoopDetectHook and ReflectRetryHook for agent self-correction#3728
Open
MuataSr wants to merge 12 commits into
Open
feat(agent): add LoopDetectHook and ReflectRetryHook for agent self-correction#3728MuataSr wants to merge 12 commits into
MuataSr wants to merge 12 commits into
Conversation
- Extend AgentHook with before_tool_call/after_tool_call lifecycle - Add CompositeHook fan-out and pipeline for per-tool hooks - Add LoopDetectHook to detect and break iteration loops - Add ReflectRetryHook to enrich tool errors with attempt history - Wire hooks into runner._run_tool execution path - 22 new tests (all passing)
This was referenced May 11, 2026
…bility - Append [Archived Context Summary] to system prompt instead of injecting it into the user message runtime context, improving KV cache reuse across turns and avoiding consecutive same-role messages. - _last_summary persists in metadata (no pop) for restart survival; summary is re-injected every turn via the stable system prompt. - Remove dynamic "Inactive for X minutes" from _format_summary — use static last_active timestamp instead to preserve KV cache stability. - Pass session_summary through build_messages() so both normal and ask_user paths receive the archived summary in the system prompt. - estimate_session_prompt_tokens now reads _last_summary from metadata to include the summary in token budget estimation. - Remove obsolete session_summary parameter from maybe_consolidate_by_tokens and estimate_session_prompt_tokens call sites in loop.py (summary flows through build_messages instead). - Ensure /new (session.clear()) clears _last_summary from metadata.
Six tests covering: - AgentDefaults preserves 'nanobot' and the cat icon by default - camelCase config keys (botName/botIcon) bind to the new fields - Empty bot_icon is accepted (opt-out of the leading icon) - ThinkingSpinner uses bot_name in its status text - StreamRenderer header combines icon and name when icon is set - StreamRenderer header is just the name when icon is empty
Two new fields with safe defaults that preserve current branding: - bot_name: str = "nanobot" - bot_icon: str = "🐈" Empty string for bot_icon is allowed and lets users opt out of the leading icon. camelCase keys (botName, botIcon) bind via the existing to_camel alias generator.
Threads bot_name/bot_icon through ThinkingSpinner and StreamRenderer with safe defaults that preserve current behavior. - ThinkingSpinner uses bot_name in its status text - StreamRenderer header is "<icon> <name>" when icon is set, or just "<name>" when icon is empty - Removes the now-unused __logo__ import (the cat emoji is the default value of bot_icon, not a hardcoded constant)
…S#3650) Both StreamRenderer instantiations in the agent command (single-message mode and interactive mode) now read bot_name and bot_icon from config.agents.defaults and forward them to the renderer. This is the wiring step that makes the schema fields actually take effect at runtime. With safe defaults of "nanobot" and "🐈", existing users see no change.
…ng (HKUDS#3585) The hosted Xiaomi MiMo API accepts {"thinking": {"type": "enabled"|"disabled"}} to toggle reasoning, which is exactly the shape produced by the existing thinking_type style. The xiaomi_mimo ProviderSpec just needed to opt in. Before this fix, setting reasoning_effort="none" had no effect on MiMo because no thinking_style was configured, so the disable signal never reached the server. Default-on models (mimo-v2.5-pro and friends) kept reasoning regardless of user configuration. Source: https://platform.xiaomimimo.com/docs/en-US/api/chat/openai-api Co-authored with Claude Opus 4.7. Strategy and review via Claude Desktop, implementation via Claude Code.
…king mode - Update AgentDefaults.reasoning_effort comment to document "none" (disable) and None (preserve provider default). - Add configuration.md tip explaining MiMo thinking mode behavior.
`crypto.randomUUID` only exists in secure contexts (HTTPS or localhost). Over LAN HTTP it is undefined, so `ChatPane`'s welcome-message flush and streaming-message handlers crash mid-render with `TypeError`, unmounting the React tree and leaving the user a blank page. Install a Math.random-backed v4-ish fallback at app entry, gated on the feature being missing. This mirrors the shim already used in the test setup and covers all six call sites (`ChatPane.tsx`, `useNanobotStream.ts`) without touching them. These IDs are client-side message keys with no security role, so non-cryptographic randomness is fine. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a focused regression test for the non-secure-context WebUI entry shim so missing crypto.randomUUID no longer depends on manual verification. Co-authored-by: Cursor <cursoragent@cursor.com>
…SDK facade _extra_hooks is meant for user-injected hooks managed by the SDK layer. Baking LoopDetectHook into it at init caused test_run_with_hooks to fail because the SDK's prev/restore pattern couldn't clean it back to []. LoopDetectHook is now an always-on default in the hook composition (line 819), independent of the mutable _extra_hooks list.
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
Agents frequently get stuck in two failure modes:
This PR adds two lightweight hooks that plug into the existing
AgentHooksystem to help agents self-correct before exhausting their iteration budget.What Changed
AgentHook— Two new hook pointsbefore_execute_tools(tools)— runs before tool batch executionafter_tool_call(name, args, result)— runs after each individual tool callLoopDetectHook(new)warn_at(default 3): injects in-context warning nudging the model to change approachhalt_at(default 5): injects stronger message explicitly telling the model to stop calling toolsReflectRetryHook(new)tool_name+sha256(args))max_retries(default 2): hint pivots to "try a fundamentally different approach"Testing
Files Changed (7 files, +560 −5)
nanobot/agent/hook.pybefore_execute_toolsandafter_tool_calltoAgentHookbase classnanobot/agent/loop_detect.pyLoopDetectHook(142 lines)nanobot/agent/reflect_retry.pyReflectRetryHook(127 lines)nanobot/agent/runner.pynanobot/agent/loop.pytests/agent/test_hook_composite.pytests/agent/test_reflect_retry.pyReflectRetryHookDesign Decisions
ReflectRetryHookdoesn't re-execute anything. It enriches error messages so the model self-corrects on the next iteration.Relates to