801 Commits

Author SHA1 Message Date
chengyongru
99cc6ee808 test(agent): expand coverage and refactor test structure
- Add 42 tests for ContextBuilder (context.py: 0→42 tests)
- Add 37 tests for SubagentManager lifecycle (subagent.py: 2→37 tests)
- Add 42 unit tests for AutoCompact in isolation
- Split monolithic test_runner.py (3313 lines) into 9 focused files:
  test_runner_core, test_runner_hooks, test_runner_errors,
  test_runner_safety, test_runner_persistence, test_runner_governance,
  test_runner_tool_execution, test_runner_injections,
  test_loop_runner_integration
- Add 3 config passthrough tests (temperature/max_tokens/reasoning_effort)
- Fix fragile patch.object(__init__) in test_stop_preserves_context
- Create shared conftest.py with make_provider/make_loop factories

Total: 934 tests passing, 0 regressions
2026-05-13 12:49:17 +08:00
chengyongru
9e15925cf4 refactor(agent): remove ask_user tool
The ask_user tool used AskUserInterrupt(BaseException) for mid-turn
blocking, creating heavy coupling across runner, loop, and session
management. The model now asks questions naturally in response text,
the turn ends normally, and the user's next message starts a new turn
with session history providing continuity.

Removed:
- nanobot/agent/tools/ask.py (tool, interrupt, helpers)
- tests/agent/test_ask_user.py
- webui/src/components/thread/AskUserPrompt.tsx
- AskUserInterrupt handling in runner.py
- Dual-path message building in loop.py
- Pending ask detection via history scanning
- button_prompt/buttons emission in WebSocket channel
- ask_user references in Slack channel docstrings

Preserved (MessageTool uses these independently):
- OutboundMessage.buttons field
- Channel button rendering (Telegram, Slack, WebSocket)
2026-05-12 22:48:26 +08:00
Xubin Ren
07f9ab580a fix(provider): preserve Bedrock tool config for history
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:59:01 +08:00
chengyongru
ef268f47d2 chore: remove dead code identified by vulture + coverage cross-validation
Remove unused code confirmed dead via vulture scan, grep verification,
and coverage analysis:

- _get_bridge_dir (cli/commands.py): 82-line function with zero callers
- add_assistant_message (agent/context.py): method body never executed,
  also removed now-unused build_assistant_message import
- _tool_parameters_schema (agent/tools/base.py): redundant copy of schema
  already exposed via the `parameters` property
- MSTEAMS_REF_TTL_S (channels/msteams.py): unused constant (production
  uses config.ref_ttl_days directly); inlined in test
- MESSAGE_TYPE_USER (channels/weixin.py): unused constant
2026-05-12 20:52:48 +08:00
Xubin Ren
079b37aac5 test(config): cover legacy model defaults without presets
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
Xubin Ren
13eede5803 refactor(agent): inject runtime model publisher
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
Xubin Ren
e6103d9312 fix(agent): separate preset snapshots from config reload
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
Xubin Ren
8fcb24bb7c refactor(agent): trim model preset runtime wiring
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
Xubin Ren
70b8daaee6 fix(command): show default as current model preset
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
Xubin Ren
c9b84c7b11 fix(config): reserve implicit default model preset
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
Xubin Ren
1d14c2ba40 fix(config): accept modelPresets root alias
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
Xubin Ren
bcc4b97183 fix(webui): broadcast runtime model updates
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
Xubin Ren
c92345bbb1 fix(webui): sync model badge after preset switch
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
Xubin Ren
b61c6304c3 fix(config): reconcile presets with settings reload
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
Xubin Ren
c450d6fd3f fix(config): make model preset switching atomic
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
chengyongru
6f78267c82 feat(config): add ModelPresetConfig and runtime preset switching
- Add `ModelPresetConfig` schema for named model presets
- Add `model_presets` dict to `Config` and `model_preset` field to `AgentDefaults`
- Add `resolve_preset()` to return effective model params from preset or defaults
- Add `@model_validator` to reject unknown preset names
- Update `_match_provider()` to use resolved preset model/provider
- Update `make_provider()` and `provider_signature()` to use `resolve_preset()`
- Add `model_preset` property to `AgentLoop` for atomic runtime switching
- Update `AgentLoop.from_config()` to inject a runtime `default` preset
- Wire self-tool to inspect/clear preset state
- Update CLI display strings to show active preset
2026-05-12 20:06:22 +08:00
Xubin Ren
1175420339 test(feishu): cover topic isolation alias
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 11:51:25 +08:00
yorkhellen
a32be99ddc test(feishu): add config and helper tests for topic_isolation 2026-05-12 11:51:25 +08:00
yorkhellen
03b357b12d feat(feishu): add topic_isolation config switch 2026-05-12 11:51:25 +08:00
Xubin Ren
fd6887c274 test(providers): cover VolcEngine token parameter
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 11:35:52 +08:00
Xubin Ren
23312d683e fix(tools): isolate plugin runtime state
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 11:28:20 +08:00
chengyongru
043f0e67f7 feat(tools): introduce plugin-based tool discovery and runtime context protocol
This commit implements a progressive refactoring of the tool system to support
plugin discovery, scoped loading, and protocol-driven runtime context injection.

Key changes:
- Add Tool ABC metadata (tool_name, _scopes) and ToolContext dataclass for
dependency injection.
- Introduce ToolLoader with pkgutil-based builtin discovery and
entry_points-based third-party plugin loading.
- Add scope filtering (core/subagent/memory) so different contexts load
appropriate tool sets.
- Introduce ContextAware protocol and RequestContext dataclass to replace
hardcoded per-tool context injection in AgentLoop.
- Add RuntimeState / MutableRuntimeState protocols to decouple MyTool from
AgentLoop.
- Migrate all built-in tools to declare scopes and implement create()/enabled()
hooks.
- Migrate MessageTool, SpawnTool, CronTool, and MyTool to ContextAware.
- Refactor AgentLoop to use ToolLoader and protocol-driven context injection.
- Refactor SubagentManager to use ToolLoader(scope="subagent") with per-run
FileStates isolation.
- Register all built-in tools via pyproject.toml entry_points.
- Add comprehensive tests for loader scopes, entry_points, ContextAware,
subagent tools, and runtime state sync.
2026-05-12 11:28:20 +08:00
04cb
bd0ba745dd fix(wecom): preserve real filename from SDK when payload omits name (#3737) 2026-05-12 10:27:32 +08:00
Alfredo Arenas
c6b7a9524c fix(providers): wire MiMo to thinking_type to allow disabling reasoning (#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.
2026-05-11 14:38:28 +08:00
Alfredo Arenas
dfb013659a test(cli): add tests for configurable bot identity (#3650)
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
2026-05-11 11:50:18 +08:00
chengyongru
a6e993df25 fix(agent): move archived summary into system prompt for KV cache stability
- 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.
2026-05-11 01:25:15 +08:00
chengyongru
733b34d685 refactor: address code review feedback on AgentLoop.from_config()
- Accept optional `provider` kwarg in from_config() to avoid double
  instantiation in _run_gateway (which already builds provider_snapshot)
- Restore try/except ValueError wrappers in serve() and agent() for
  clean error messages on provider creation failure
- Update test: _FakeAgentLoop captures provider from kwargs, restore
  strong assertion (seen["provider"] is provider)
2026-05-09 15:30:48 +08:00
chengyongru
3202f58c41 refactor: introduce AgentLoop.from_config() to centralize loop assembly
Extract duplicated bus/provider/loop initialization from CLI commands
(serve, _run_gateway, agent) and Nanobot facade into a single
AgentLoop.from_config() classmethod.

- Remove _make_provider() from cli/commands.py and nanobot.py
- Remove inline provider creation in all three CLI entry points
- AgentLoop.from_config() creates MessageBus, calls make_provider(),
  and assembles AgentLoop with all standard config-derived parameters
- Supports **extra overrides for callers that need custom args
  (e.g. cron_service, session_manager, provider_snapshot_loader)
- Update tests to mock make_provider at nanobot.providers.factory
  and add from_config classmethod to _FakeAgentLoop fixtures

This is PR 1/4 of the model-preset feature decomposition.
2026-05-09 15:30:48 +08:00
Xubin Ren
9252f4d826 Revert "fix(agent): persist _last_summary across restarts with used sentinel"
This reverts commit e5a1416a37b423de95b0fa279e9473110a678112.
2026-05-09 15:00:54 +08:00
chengyongru
e5a1416a37 fix(agent): persist _last_summary across restarts with used sentinel
The previous implementation popped _last_summary from session.metadata
after injecting it into the prompt, then saved the session. This caused
the summary to be permanently lost after a process restart, making the
AI forget archived context and appear to ignore memory or reference
non-existent previous messages.

Replace the destructive pop with a _last_summary_used sentinel:
- _last_summary stays in metadata for restart survival
- _last_summary_used prevents duplicate injection within the same turn
- Clear the sentinel whenever a new summary is generated

Updates tests to match the new persistence behavior.
2026-05-09 14:58:38 +08:00
Xubin Ren
56eee06736 feat(webui): add BYOK web search settings
Let WebUI users configure the single web search provider credential from BYOK while keeping saved secrets masked and hot-reloaded for new searches.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 14:52:48 +08:00
Xubin Ren
3231aaf9ee fix(image): prevent duplicate delivery and replay artifacts 2026-05-09 05:45:13 +00:00
yorkhellen
843e96f09d fix(feishu): send all messages to topic when in thread 2026-05-09 01:03:57 +08:00
chengyongru
908f1246d8 fix(cli): sanitize surrogate code points before entering message bus
On Windows, prompt_toolkit produces lone surrogate code points (e.g.
🐈) for emoji input. These propagate through the message bus
and crash at json.dumps() / file write time because surrogates cannot
be encoded as UTF-8.

Extract _sanitize_surrogates() that round-trips through UTF-16 to
reconstruct paired surrogates into real characters (e.g. 🐈🐈), replacing unpaired surrogates with U+FFFD. Apply it at the CLI
input path and reuse in SafeFileHistory.
2026-05-09 01:03:34 +08:00
Xubin Ren
2cc32ca07c feat(webui): redesign settings and BYOK configuration
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 23:48:01 +08:00
Xubin Ren
cbd5b06075 fix(memory): align replay overflow with history trimming
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 20:37:03 +08:00
Xubin Ren
24daf9a51c test(memory): accept replay window in consolidation assertion
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 20:37:03 +08:00
Xubin Ren
91ade9eaac fix(memory): consolidate history hidden by replay window
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 20:37:03 +08:00
Xubin Ren
2c830ca817 test(weixin): stabilize typing keepalive assertion
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 20:06:23 +08:00
Xubin Ren
e936ed48bd feat: add image generation tool and WebUI mode
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 20:06:23 +08:00
chengyongru
3a2f47d720 fix(onboard): allow empty strings and falsy values in input fields
Fixes two related input-handling bugs in the onboard wizard:

1. _input_text treated "" as None, preventing users from clearing
   optional string fields or entering empty strings intentionally.

2. _input_model_with_autocomplete used `if value else None`, which
   discarded falsy values such as empty strings or 0.

To support clearing optional string fields, add _is_str_or_none() and
normalize empty strings to None inside _configure_pydantic_model only
when the field annotation is `str | None`. Required str fields keep
"" as a valid value.

Also included:
- Remember last selected item in provider/channel/model menus for
  better UX when configuring multiple items.
- Rename _SIMPLE_TYPES and _MENU_DISPATCH to lowercase to follow
  Python naming conventions (they are local variables, not constants).
- Remove unused imports in test file.

Extracted from PR #3358.
2026-05-08 13:21:51 +08:00
Jefsky
44a341335a fix(dream): restore cursor with memory state
Track the Dream cursor in memory versioning so restores do not skip history after rolling back Dream commits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 01:06:05 +08:00
Xubin Ren
ac18a8baad feat(webui): add localized slash commands
Add a session-scoped slash command palette sourced from backend command metadata, and keep welcome-page quick actions localized across all WebUI languages.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 00:20:28 +08:00
chengyongru
49c07aa45a style: address code review feedback
- Consistent "WeChat" prefix in context_token error message
- Use object() instead of httpx.AsyncClient() in new tests to avoid
  resource leak warnings
2026-05-06 23:52:50 +08:00
chengyongru
98c2f7cc27 fix(weixin): raise exceptions instead of silently dropping messages
_send_text() swallowed API errors (non-zero errcode) with just a
warning log, and send() had three silent return paths (no client,
session paused, no context_token). Neither triggered ChannelManager's
retry logic, causing persistent message loss until a new inbound
message refreshed the context_token.

Now all failure paths raise RuntimeError, matching BaseChannel's
contract and enabling proper retry behavior.
2026-05-06 23:52:50 +08:00
chengyongru
4efd904ccc fix(webui): require token_issue_secret for LAN access with frontend auth
When host is set to 0.0.0.0, the gateway now enforces that either token
or token_issue_secret must be configured — it refuses to start otherwise.

Bootstrap endpoint behavior:
- token_issue_secret configured: always validate regardless of source IP
  (handles reverse-proxy scenarios where all connections appear as localhost)
- No secret: only localhost can bootstrap (local dev mode)

The frontend shows an authentication form when bootstrap returns 401/403,
persists the secret in localStorage, and retries automatically on reload.
2026-05-06 23:51:51 +08:00
chengyongru
034bea1a44 fix(webui): require token_issue_secret for non-localhost bootstrap
The previous LAN-access fix (PR #3656) relaxed the bootstrap localhost
check when host was 0.0.0.0, but did not require any authentication —
any device on the network could obtain a token without credentials.

New behavior:
- token_issue_secret configured: always validate, regardless of source
  IP (handles reverse-proxy scenarios where all connections appear as
  localhost).
- No secret configured: only localhost can bootstrap (local dev mode).

This supersedes the host-based check from PR #3656.
2026-05-06 23:51:51 +08:00
chengyongru
bad584cb0e fix(webui): allow LAN access when host is 0.0.0.0
The webui bootstrap endpoint (/webui/bootstrap) rejected all non-localhost
connections with HTTP 403, preventing the embedded webui from working when
accessed from another device on the LAN — even when host was set to 0.0.0.0.

Skip the localhost check when the server is explicitly bound to 0.0.0.0 or ::,
since that signals intent to accept external connections.
2026-05-06 23:00:23 +08:00
Xubin Ren
790a03ec28 feat(webui): polish chat layout and titles
Align the WebUI sidebar and chat chrome with the updated design, and generate WebUI session titles asynchronously without blocking turns.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 22:20:35 +08:00
chengyongru
40b4e01b13 merge: resolve conflict with main in transcription.py
Keep _post_transcription_with_retry from PR branch, drop inline
httpx calls that were replaced by the shared retry helper.
2026-05-06 21:26:28 +08:00