Commit Graph
292 Commits
Author SHA1 Message Date
chengyongruandXubin Ren b2fb776a68 refactor(loop): extract _build_bus_progress_callback 2026-05-09 17:15:23 +08:00
chengyongruandXubin Ren ff6b014a07 refactor: allow model/context_window_tokens override in from_config()
- Pop model and context_window_tokens from extra kwargs before
  forwarding to __init__, allowing callers like _run_gateway to
  pass snapshot-derived values instead of config defaults
- _run_gateway now explicitly passes model/context_window_tokens
  from provider_snapshot to preserve pre-refactor behavior
2026-05-09 15:30:48 +08:00
chengyongruandXubin Ren 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
chengyongruandXubin Ren 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
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
91ade9eaac fix(memory): consolidate history hidden by replay window
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 20:37:03 +08:00
e936ed48bd feat: add image generation tool and WebUI mode
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 20:06:23 +08:00
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
Tim O'BrienandXubin Ren 67875d7a15 fix: wire toolHintMaxLength through AgentLoop constructors
The config field was added but never passed from config to AgentLoop.
The value was always falling back to the default (40) regardless of
what was set in config.json.

Now passes tool_hint_max_length through all AgentLoop() call sites:
- nanobot/nanobot.py (main bot)
- nanobot/cli/commands.py (CLI agent, dev, webui commands)

Also adds documentation in docs/configuration.md.
2026-05-06 21:18:39 +08:00
Tim O'BrienandXubin Ren daa4a25c9b feat(config): add toolHintMaxLength to control tool hint truncation
Add  to  config (default: 40, range: 20-500).
Controls how many characters of tool hints are shown in progress updates
(e.g. '$ cd …/project && npm test').

Set to 120+ to see full commands instead of truncated hints:

```json
{
  "agents": {
    "defaults": {
      "toolHintMaxLength": 120
    }
  }
}
```

- Thread max_length through format_tool_hints → _fmt_known/_fmt_mcp/_fmt_fallback
- Make path abbreviation in _abbreviate_command proportional to max_length
- Add TestToolHintMaxLength test class with 5 tests
- All 41 existing tests pass
2026-05-06 21:18:39 +08:00
hanyuanlingandXubin Ren 653de4a7ef fix(agent): gate provider progress deltas 2026-05-06 21:18:30 +08:00
chengyongruandXubin Ren 05e0106592 refactor(logging): preserve tracebacks and add channel context
- Preserve tracebacks: logger.error in except blocks → logger.exception
- Channel context: BaseChannel injects self.logger = logger.bind(channel=name)
- Third-party bridge: redirect_lib_logging() replaces ad-hoc stdlib-to-loguru bridges
- Log levels: network timeouts downgraded from ERROR → WARNING
- Fix --verbose flag to actually work with loguru (set handler to DEBUG)
2026-05-06 21:17:45 +08:00
chengyongruandXubin Ren d3689d143c fix(agent): prevent safety guard false positives and streamed message drop
Three independent fixes for issues exposed by PR #3493:

1. shell.py: allow /dev/* paths in workspace guard
   Commands like `rm file.txt 2>/dev/null` were blocked because
   _extract_absolute_paths captured /dev/null as a path outside
   the workspace. Allow /dev like media_path is already allowed.

2. shell.py: remove | from home_paths regex prefix
   Loki query operator `|~` was misinterpreted as pipe + home
   directory, causing false workspace violation errors.

3. loop.py: change _streamed from blacklist to whitelist
   stop_reason "tool_error" was not in the exclusion set
   {"ask_user", "error"}, so _streamed=True was set on fatal
   errors. channel manager then skipped channel.send() because
   it assumed the content was already streamed — but it never
   was. Whitelist to only {"stop", "end_turn", "max_tokens"}.

Also fixes a pre-existing Windows bug in _spawn where
create_subprocess_exec + list2cmdline breaks commands with
paths containing spaces (e.g. D:\Program Files\python.exe).

Closes: #3599, #3605
2026-05-04 01:25:52 +08:00
96da6d8190 fix(webui): tighten turn completion handling
Keep the new turn-end signal scoped to WebSocket clients, preserve pending tool-call state across trailing tool result rows, and drop the accidental npm lockfile from the Bun-based WebUI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 22:28:40 +08:00
ramonpaoloandXubin Ren 76e3f74df7 feat(webui): improve beta turn completion and streaming UX 2026-05-03 22:28:40 +08:00
5853d5dfda fix: allow_patterns take priority over deny_patterns in ExecTool (#3594)
* fix: allow_patterns take priority over deny_patterns in ExecTool

Previously deny_patterns were checked first with no bypass, meaning
allow_patterns could never exempt commands from the built-in deny list.
This made it impossible to whitelist destructive commands for specific
directories (e.g. build/cleanup tasks).

Changes:
- shell.py: check allow_patterns first; if matched, skip deny check
- shell.py: deny_patterns now appends to built-in list (not replaces)
- schema.py: add allow_patterns/deny_patterns to ExecToolConfig
- loop.py/subagent.py: pass allow_patterns/deny_patterns to ExecTool
- Add test_exec_allow_patterns.py covering priority semantics

* fix: separate deny pattern errors from workspace violation detection

The deny pattern error message "Command blocked by safety guard" was
included in _WORKSPACE_BLOCK_MARKERS, causing deny_pattern blocks to be
misclassified as fatal workspace violations. This meant LLMs had no
chance to retry with a different command — the turn was aborted
immediately.

Changes:
- shell.py: deny/allowlist error messages now use distinct phrasing
  ("blocked by deny pattern filter" / "blocked by allowlist filter")
- runner.py: remove "blocked by safety guard" from
  _WORKSPACE_BLOCK_MARKERS so deny_pattern errors are treated as normal
  tool errors (LLM can retry) instead of fatal violations
- workspace path errors still use "blocked by safety guard" and remain
  fatal as intended

* fix: update test assertions to match new deny pattern error message

* fix: indentation error in test file

* fix: restore SSRF fatal classification and tidy exec pattern plumbing

Address review feedback on the deny/allow_patterns rework:

- runner.py: re-add "internal/private url detected" to
  _WORKSPACE_BLOCK_MARKERS. The earlier marker removal also stripped
  fatal classification from SSRF / internal-URL rejections (whose
  message still says "blocked by safety guard"), turning a hard
  security boundary into something the LLM could retry.
- loop.py / subagent.py: drop `or None` between ExecToolConfig and
  ExecTool. The schema default is an empty list and ExecTool already
  normalizes None back to [], so the indirection was a no-op.
- shell.py: extract `explicitly_allowed` flag in _guard_command so
  allow_patterns are scanned once instead of twice and the control
  flow no longer relies on a no-op `pass + else` branch.
- tests/agent/test_runner.py: add a regression test asserting that
  the SSRF block message is treated as fatal, while deny/allowlist
  filter messages are deliberately non-fatal.

* fix: remove unused exec allow-pattern test import

Keep the new ExecTool allow-pattern coverage clean under ruff.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Xubin Ren <xubinrencs@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 00:27:17 +08:00
Xubin Ren e157392250 fix(agent): scope subagent reply dedupe to origin message
Made-with: Cursor
2026-05-01 11:47:24 +00:00
yorkhellenandXubin Ren c4170fa9ba feat: Add sender_id to LLM runtime context 2026-05-01 19:43:38 +08:00
Jack LuandXubin Ren d9800ecdd2 refactor: replace try-except blocks with contextlib.suppress for cleaner error handling across multiple files 2026-05-01 19:30:11 +08:00
Xubin RenandXubin Ren 39c38b593f refactor(tools): move file state lookup out of loop
Made-with: Cursor
2026-05-01 19:15:07 +08:00
Xubin RenandXubin Ren fae38319ca fix(tools): scope file state by session
Made-with: Cursor
2026-05-01 19:15:07 +08:00
LZDQandXubin Ren 58ae2d5b7e Claude: replace module-level file read states with per-loop per-session state class. fixes #3571 2026-05-01 19:15:07 +08:00
hanyuanlingandXubin Ren 3c20d16117 fix subagent max iteration limit 2026-04-30 13:45:40 +08:00
Xubin Ren 18432c313f Merge origin/main into web-tools
Made-with: Cursor
2026-04-28 07:17:05 +00:00
Xubin RenandXubin Ren ad4802600e refactor(config): make max messages default explicit
Use 120 as the config-level default and normalize zero back to that limit so session replay always receives an explicit message cap.

Made-with: Cursor
2026-04-28 14:54:32 +08:00
hussein1362andXubin Ren d45ffcf519 feat(config): wire max_messages into session history replay
The max_messages config field in AgentDefaults was accepted by the
schema but never threaded through to the actual get_history() calls
in the agent loop.  Both call sites in _process_message hardcoded the
default, so sessions with slow or local models accumulated unbounded
history that inflated prompt tokens and caused LLM timeouts.

Changes:
- Add max_messages field to AgentDefaults (default 0 = use built-in
  constant, any positive value caps history replay)
- Store the value on AgentLoop and pass it to get_history() when
  non-zero
- Wire the config through all three AgentLoop construction sites in
  commands.py (gateway, API server, CLI chat)
- 14 focused tests covering schema validation, init storage, history
  slicing, boundary alignment, integration wiring, and the
  zero/default path
2026-04-28 14:54:32 +08:00
hanyuanlingandXubin Ren ae14142a87 fix(codex): stream progress deltas to channels 2026-04-27 18:48:05 +08:00
Xubin Ren e31273ebaa Merge origin/main into fix/discord-allow-channel-threads
Made-with: Cursor
2026-04-27 09:26:24 +00:00
Xubin Ren eb4b3d9e26 refactor(session): internalize history/file-cap knobs as constants
Move sessionHistoryMaxMessages, sessionHistoryMaxTokens, and
sessionFileMaxMessages out of user-facing config into internal
constants (HISTORY_MAX_MESSAGES=120, FILE_MAX_MESSAGES=2000).

- Remove 3 fields from AgentDefaults and config pipeline
- Sink enforce_file_cap into Session (was AgentLoop)
- Auto-derive token budget from context window (was configurable)
- Net -113 lines across 7 files; 723 tests green

Made-with: Cursor
2026-04-27 08:06:50 +00:00
Xubin Ren 29ebc2d355 Merge origin/main into feat/session-replay-file-cap-invariants
Preserve main's timestamp/tool-context replay semantics while keeping the PR's session history and file-cap budgets.

Made-with: Cursor
2026-04-27 07:32:00 +00:00
mt-huertaandXubin Ren 380309016a fix(agent): complete thread-session routing for spawn dispatch and system-channel branch
Builds on PR #3463 (commit 038a140), which introduced metadata and
session_key parameters through _LoopHook and _set_tool_context for the
cron and message tools. Three downstream gaps remained:

1. _set_tool_context's body still computes effective_key from
   channel:chat_id and passes that to spawn, even when the caller
   provides a thread-scoped session_key. The new parameter is wired in
   for cron/message but spawn dispatch ignores it. Result: subagent
   announces from threaded callers carry a channel-only
   session_key_override, dropping thread_ts.

2. _process_message's system-channel branch loads the session via
   key = f"{channel}:{chat_id}", ignoring msg.session_key_override.
   So even when the announce InboundMessage carries the right override
   (after fix 1), the consumer side discards it and routes to the
   channel-level session.

3. The OutboundMessage returned from the system-channel branch has no
   metadata, so slack's outbound dispatcher has no thread_ts to use and
   posts the LLM's reply to the channel top-level rather than the
   originating thread.

This change closes all three gaps with three small edits in loop.py.

Behavior change:
- Slack channels with reply_in_thread: true: subagent announces and
  follow-up replies now arrive in the originating thread session
  instead of leaking into the channel-level session.
- Other channels constructing thread-scoped session keys (matrix
  threads, telegram thread mode, etc.): the session-loading and
  effective-key fixes apply identically since they're platform-agnostic.
  The outbound thread_ts reconstruction is slack-specific by virtue of
  the session-key format slack uses; other channels would benefit from
  the same pattern but are out of scope for this PR.
- Unified session mode: no change. Falls back to UNIFIED_SESSION_KEY
  when session_key is not provided.
- CLI / non-channel callers: no change. They don't pass session_key
  and the fallback to f"{channel}:{chat_id}" matches prior behavior.

Reproducer (slack with reply_in_thread: true):
1. From a slack thread, send a message that triggers a subagent spawn.
2. Before fix: announce lands in slack:<channel>.jsonl session,
   parent agent in the thread never sees the completion event,
   eventual reply (if any) posts to the channel top-level, not the
   thread.
3. After fix: announce lands in slack:<channel>:<thread_ts>.jsonl,
   parent agent in the thread responds within seconds, reply posts in
   the thread.
2026-04-27 14:37:36 +08:00
Xubin RenandXubin Ren 9b6f3d7abc fix(agent): resolve message media against active workspace
Made-with: Cursor
2026-04-27 14:31:39 +08:00
Xubin Ren 4a4ba1efc1 Merge branch 'main' into fix/session-history-timestamps
Made-with: Cursor
2026-04-26 18:13:11 +00:00
Xubin RenandXubin Ren 038a140ad3 fix(slack): preserve thread context for proactive replies
Capture Slack thread metadata for cron and message-tool deliveries so replies stay in the originating thread, and hydrate first thread mentions with recent Slack context.

Made-with: Cursor
2026-04-27 02:10:38 +08:00
Xubin Ren df37a36174 fix(agent): expose session timestamps in model context
Include persisted turn timestamps when assembling LLM prompts so relative-date references like yesterday and today have concrete anchors.

Made-with: Cursor
2026-04-26 17:42:58 +00:00
hanyuanling 59dfd74842 feat(session): enforce replay/file-cap invariants for history lifecycle 2026-04-27 00:53:32 +08:00
Xubin Ren b2aec5528a refactor(agent): move provider refresh into subsystem owners 2026-04-26 14:18:37 +00:00
Xubin Ren f670da6c70 refactor(providers): move provider snapshot creation into factory 2026-04-26 14:05:13 +00:00
Xubin Ren 65b0ae81af Merge origin/main into webui-settings
Made-with: Cursor
2026-04-26 13:05:32 +00:00
SubalandXubin Ren 80ee4483f8 feat: make consolidation ratio configurable 2026-04-26 20:24:42 +08:00
Xubin Ren b440e76d2f feat(webui): add model settings runtime refresh 2026-04-25 18:05:06 +00:00
Xubin RenandXubin Ren 403ce23d22 fix(agent): tighten ask_user CLI handling
Made-with: Cursor
2026-04-25 22:10:19 +08:00
Xubin RenandXubin Ren 3b1ea99ee1 fix(agent): render ask_user options without buttons
Made-with: Cursor
2026-04-25 22:10:19 +08:00
Xubin RenandXubin Ren cfc76ffbbf feat(agent): add ask_user tool
Made-with: Cursor
2026-04-25 22:10:19 +08:00
Xubin RenandXubin Ren 52855d463e refactor(agent): move progress event helpers out of loop
Made-with: Cursor
2026-04-23 20:06:11 +08:00
Xubin RenandXubin Ren 469fc90fe6 fix(agent): on_progress tool_events only when callback accepts; align progress tests with main
Made-with: Cursor
2026-04-23 20:06:11 +08:00
c23d719780 feat(agent): emit structured _tool_events progress metadata
Extend the existing on_progress callback to carry structured tool-event
payloads alongside the plain-text hint, so channels can render rich
tool execution state (start/finish/error, arguments, results, file
attachments) rather than only the pre-formatted hint string.

Changes
-------
- AgentLoop._tool_event_start_payload() — builds a version-1 start
  payload from a ToolCallRequest
- AgentLoop._tool_event_result_extras() — extracts files/embeds from a
  tool result dict
- AgentLoop._tool_event_finish_payloads() — maps tool_calls +
  tool_results + tool_events from AgentHookContext into finish payloads
- _LoopHook.before_execute_tools() — passes tool_events=[...] to
  on_progress together with the existing tool_hint flag
- _LoopHook.after_iteration() — emits a second on_progress call with
  the finish payloads once tool results are available
- _bus_progress() — forwards tool_events as _tool_events in OutboundMessage
  metadata so channel implementations can read them
- on_progress type widened to Callable[..., Awaitable[None]] on all
  public entry points; _cli_progress updated to accept and ignore
  tool_events

The contract is additive: callers that only accept (content, *, tool_hint)
continue to work unchanged. Callers that also accept tool_events receive
the structured data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 20:06:11 +08:00
Bongjin Lee 93ca791ac6 fix(discord): full thread support with session isolation and allowlist enforcement
Discord threads use their own channel IDs, so allowChannels was blocking
thread replies unless each thread ID was listed explicitly.

- Include the thread parent channel ID as an allowlist candidate
- Enforce allow_channels on slash commands (previously bypassed)
- Show parent channel ID in runtime context, reply to the thread
- Fix subagent cancel key via effective_key propagation
- Detect bot mentions via raw_mentions and reply-to-bot references
- Cache seen thread channels for outbound delivery
- Ignore system messages that become empty prompts
2026-04-23 04:05:39 +09:00
Xubin RenandXubin Ren 61a28c2c0a feat(webui): support image uploads in composer and message bubbles 2026-04-23 00:07:27 +08:00
chengyongruandXubin Ren 42c4af2118 fix(agent): prevent duplicate responses when sub-agents complete concurrently
When the main agent spawns multiple sub-agents, each completion
independently triggered a new _dispatch, causing 3-4 user-visible
responses instead of a single comprehensive report.

- Extend _drain_pending to block-wait on pending_queue when sub-agents
  are still running, keeping the runner loop alive for in-order injection
- Pass pending_queue in the system message path so subsequent sub-agent
  results can still be injected mid-turn via a new dispatch
2026-04-22 20:02:19 +08:00