Commit Graph
100 Commits
Author SHA1 Message Date
Xubin Ren 897eedaaa7 chore(ci): update Python version in CI workflow to focus on supported runtimes 3.13 and 3.14 2026-05-16 04:15:58 +00:00
Xubin Ren 9ccef018c2 feat(telegram): add new slash commands and update regex for command handling 2026-05-15 17:55:52 +00:00
0f96ab7e70 fix(webui): drop App markdown warmup; keep preloadMarkdownText export
Startup no longer triggers preloadMarkdownText (#3746). Restore the named
export so MessageBubble can still warm the lazy markdown chunk when the
reasoning panel opens (compatible with current main).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 01:42:42 +08:00
Xubin Ren 0a25f696ab chore(docs): refine README entry for 2026-05-08 to clarify inline chat image feature 2026-05-15 17:35:56 +00:00
Xubin Ren 4fbabb5474 chore(docs): update README with recent news entries and earlier updates for clarity 2026-05-15 17:35:28 +00:00
Xubin Ren 937c8e6931 chore(docs): update README with recent news entries and earlier updates 2026-05-15 17:32:16 +00:00
Xubin Ren 858b6610c3 fix(config): reduce max_tokens and context_window_tokens in schema 2026-05-15 17:19:47 +00:00
Xubin Ren 5d7f3f2751 fix(webui): stabilize live thread rendering and navigation 2026-05-13 16:39:07 +00:00
Xubin RenandCursor 5efd67919b feat(runner): support fallback candidates
Resolve fallbackModels as preset references or explicit inline provider configs so failover uses complete model settings without exposing fallback logic to the agent loop.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 15:34:03 +00:00
Xubin Ren 43db848db0 Revert "feat(runner): support structured fallback models"
This reverts commit 02b059a616.
2026-05-13 14:11:08 +00:00
Xubin RenandCursor 02b059a616 feat(runner): support structured fallback models
Bind fallback model chains to the active model configuration so defaults and presets do not inherit or merge fallback behavior implicitly. Require explicit fallback providers while preserving per-fallback generation overrides and context-window safety.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 13:57:30 +00:00
Xubin Ren eaa8ebd5d3 Merge remote-tracking branch 'origin/main' into pr-3756 2026-05-13 13:12:56 +00:00
Xubin Ren fb508a302a feat(webui): refresh session titles from live updates 2026-05-13 13:10:21 +00:00
Xubin RenandCursor 567e95dee6 fix(cli): stop spinner before resumed answer deltas
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 09:18:59 +00:00
Xubin RenandCursor 53831e1611 fix(cli): clear thinking spinner before trace output
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 09:15:53 +00:00
Xubin RenandCursor 3fab736262 fix(cli): keep trace output under assistant header
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 09:13:16 +00:00
Xubin RenandCursor 9d50f1b933 feat: polish trace delivery and slash menu UX
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 08:47:34 +00:00
Xubin RenandCursor 321c565ec4 fix(webui): normalize thinking trace row box model
Thinking and Used tools are both auxiliary rows, but Thinking still carried
an internal mb-2 even when it was standalone. That made collapsed Thinking
rows visually taller than tool trace rows despite the shared thread spacing.

Only add the extra bottom margin when a Thinking bubble has answer content
below it in the same assistant message. Standalone Thinking rows now share
the same outer box model as Used tools. Tests lock both standalone and
answer-backed cases.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 08:12:44 +00:00
Xubin RenandCursor 82ba63e148 fix(webui): compact spacing between auxiliary trace rows
Thinking and Used tools are both auxiliary trace rows, but the thread list
was applying the same large gap used between full chat turns. That made
alternating Thinking / Used tools sequences look uneven and too airy.

Move row spacing from a fixed flex gap to per-row margins: full chat turns
keep mt-5, while consecutive auxiliary rows use mt-2. Add coverage for
Thinking -> Used tools -> Thinking spacing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 08:05:34 +00:00
Xubin RenandCursor c7ec5d3b75 fix(webui): align thinking and tool trace affordances
Tool trace groups are supporting details, so default them to collapsed.
Match the Thinking bubble's expanded body to the tool trace affordance by
using the same grouped header and animated fade/slide body treatment.

Update MessageBubble tests to assert tool traces start collapsed and expand
on click.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 07:58:24 +00:00
Xubin RenandCursor 521aaa5ecf fix(webui): split reasoning at tool trace boundaries
Live rendering merged reasoning chunks by scanning backward to the latest
assistant row. That fixed late reasoning, but the scan skipped trace rows,
so reasoning after a tool call crossed the Used tools block and attached to
the previous assistant iteration. Refresh looked correct because persisted
history reconstructs assistant/tool boundaries.

Treat trace rows as hard phase boundaries, just like user messages. A
reasoning_delta after Used tools now starts a fresh assistant placeholder,
so live rendering matches replay: Thinking -> Used tools -> Thinking ->
Used tools / answer.

Add a regression for reasoning_delta -> reasoning_end -> tool_hint ->
reasoning_delta.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 07:49:44 +00:00
Xubin RenandCursor 278affc25e fix(webui): hydrate reasoning and tool traces from history
Live reasoning/tool frames were rendering correctly, but refreshing WebUI
replayed only role/content/media from `/api/sessions/:key/messages`.
Assistant `reasoning_content` / `thinking_blocks` and `tool_calls` were
already persisted by the backend and returned by the history endpoint, but
useSessionHistory discarded them.

Hydrate persisted assistant reasoning into `UIMessage.reasoning` and
reconstruct assistant tool calls as `kind: "trace"` rows so the replayed
thread keeps the same Thinking bubble and Used tools block as the live
stream. Tool result rows remain hidden from the conversation view to avoid
replaying raw tool output as chat text.

Adds regression coverage for both persisted reasoning and historical tool
call trace hydration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 07:33:52 +00:00
Xubin RenandCursor 0033a8a185 fix(webui): keep reasoning scoped to the current user turn
The post-hoc reasoning fix allowed late reasoning frames to attach back to
the nearest assistant message, but the scan crossed a newer user message.
That made the next turn's Thinking bubble render above the previous
assistant reply.

Treat the latest user message as a hard boundary: reasoning after it must
start a new assistant placeholder and can no longer attach to earlier
assistant turns. Add a regression covering previous assistant -> new user
-> reasoning_delta.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 07:28:54 +00:00
Xubin RenandCursor 9829cf66d2 fix(webui): keep late reasoning attached above the answer
Some providers only surface structured `reasoning_content` after answer
text has already streamed. The WebUI was treating those late
`reasoning_delta` frames as a fresh assistant placeholder, so the
Thinking bubble rendered below the already-visible answer.

Attach late reasoning back to the active assistant turn instead. The
bubble still renders above the message content, preserving the expected
Thinking -> answer order even when the provider protocol delivers the
reasoning post-hoc. Added a regression test for answer-first followed by
reasoning_delta/reasoning_end.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 07:20:36 +00:00
Xubin RenandCursor 458b4ba235 feat(reasoning): stream reasoning content as a first-class channel
Reasoning now flows as its own stream — symmetric to the answer's
``delta`` / ``stream_end`` pair — instead of being shipped as one
oversized progress message. This lets WebUI render a live "Thinking…"
bubble that updates in place, then auto-collapses when the stream
closes. Other channels remain plugin no-ops by default.

## Protocol

New metadata: ``_reasoning_delta`` (chunk) and ``_reasoning_end``
(close marker). ChannelManager routes both to the dedicated plugin
hooks below; the legacy one-shot ``_reasoning`` is kept for back-compat
and BaseChannel expands it into a single delta + end pair so plugins
only ever implement the streaming primitives.

WebSocket emits two new events:

- ``reasoning_delta`` (event, chat_id, text, optional stream_id)
- ``reasoning_end`` (event, chat_id, optional stream_id)

## BaseChannel surface

- ``send_reasoning_delta(chat_id, delta, metadata)`` — no-op default
- ``send_reasoning_end(chat_id, metadata)`` — no-op default
- ``send_reasoning(msg)`` — back-compat wrapper, base impl forwards
  to the streaming primitives

A channel adds reasoning support by overriding the two streaming
primitives. Telegram / Slack / Discord / Feishu / WeChat / Matrix keep
the base no-ops until their bubble UIs are adapted; reasoning silently
drops at dispatch, never as a stray text message.

## AgentHook

Adds ``emit_reasoning_end`` to the hook lifecycle. ``_LoopHook`` tracks
whether a reasoning segment is open and closes it on:

- the first answer delta arriving (so the UI locks the bubble before
  the answer renders below),
- ``on_stream_end``,
- one-shot ``reasoning_content`` / ``thinking_blocks`` after a single
  non-streaming response.

## WebUI

- ``UIMessage.reasoning`` is now a single accumulated string with a
  companion ``reasoningStreaming`` flag.
- ``useNanobotStream`` consumes ``reasoning_delta`` / ``reasoning_end``;
  legacy ``kind: "reasoning"`` is auto-translated to a delta + end.
- New ``ReasoningBubble``: shimmer header + auto-expanded while
  streaming, collapses to a clickable "Thinking" pill once closed,
  respects ``prefers-reduced-motion``.
- Answer deltas adopt the reasoning placeholder so the bubble and the
  answer share one assistant row.

## Tests

- ``tests/channels/test_channel_manager_reasoning.py`` — manager routes
  delta + end, drops on channel opt-out, expands one-shot back-compat.
- ``tests/channels/test_websocket_channel.py`` — new ``reasoning_delta``
  / ``reasoning_end`` frames, empty-chunk safety, no-subscriber safety,
  back-compat expansion.
- ``tests/agent/test_runner_reasoning.py`` — runner closes the segment
  on streaming answer start and after one-shot reasoning.
- WebUI ``useNanobotStream`` + ``message-bubble`` cover the new
  protocol and the shimmer styling.

## Docs

``docs/configuration.md`` and ``docs/websocket.md`` document the new
events and the plugin contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 07:13:43 +00:00
Xubin RenandCursor a6b059d379 refactor(reasoning): make channel plugins own reasoning rendering
Reasoning was being shipped to every channel as a generic progress
message with a `_reasoning: true` flag. Two problems with that:

1. Channels without a low-emphasis UI primitive (Telegram, Slack,
   Discord, Feishu...) would dump raw model thoughts as ordinary
   replies, polluting the conversation.
2. The agent loop double-gated by inspecting `channels_config`, which
   coupled the loop to display policy.

Treat reasoning as its own plugin action — `BaseChannel.send_reasoning`
defaults to a documented no-op; channels that have a fitting affordance
override. ChannelManager routes `_reasoning` outbounds to that method
only when the channel opts in via `show_reasoning` (camelCase alias
`showReasoning` mirrors `sendProgress`). Plugins that don't override
silently drop reasoning — "no fit, no leak" is the contract.

Reference implementation lands for WebSocket / WebUI: a new
`kind: "reasoning"` frame, parked on the active assistant bubble as a
collapsible `Thinking` group above the answer. CLI keeps its existing
direct path (it doesn't go through the bus). `ChannelsConfig.show_reasoning`
flips to `true` by default — only adapted channels surface anything,
others stay quiet.

Loop net diff is -3 lines: the `channels_config.show_reasoning` check
moves out, leaving emit_reasoning a one-liner that publishes and trusts
the channel to decide.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 06:27:53 +00:00
Xubin RenandCursor 01fa362c03 Merge origin/main into feat/show-reasoning
Resolves conflicts after main landed the state-machine turn refactor
and the test_runner.py 9-file split:

- nanobot/agent/loop.py: take main's `_state_build`/`_persist_user_message_early`
  flow; restore the `reasoning: bool` parameter on `_build_bus_progress_callback`
  so the loop hook can mark progress as reasoning-channel without coupling to
  the answer stream.
- nanobot/cli/stream.py: keep main's configurable `bot_name`/`bot_icon` header
  while preserving the PR's `transient=True` Live + `self._console` routing
  + `_renderable()` final-render path that fixed TUI duplication.
- tests/agent/test_runner.py was deleted on main and split into 9 focused
  files; relocated all 6 reasoning tests into a new `test_runner_reasoning.py`
  matching the new layout, deduplicated the per-test `ReasoningHook` boilerplate
  through a shared `_RecordingHook` helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 05:07:14 +00:00
Xubin RenandCursor 352aaf0627 refactor(reasoning): unify reasoning extraction across providers
Reasoning surfacing was split across three branches in runner.py plus
two separate streaming buffers (loop hook and runner progress stream),
with three independent display-side gates in the CLI. This collapsed
the policy into one source of truth and fixed two real bugs:

- Structured `reasoning_content` was suppressed whenever the answer was
  streamed, because the runner gated emission on `streamed_content`.
  Providers don't stream `reasoning_content`; it only arrives on the
  final response, so the answer stream and the reasoning channel are
  independent. Added `streamed_reasoning` to `AgentHookContext` to track
  the right bit.
- `channels.showReasoning` was subordinated to `sendProgress`. They are
  orthogonal — turning off progress streaming shouldn't silence
  reasoning. Reworked the CLI gates accordingly.

Single-helper consolidation:

- `extract_reasoning(reasoning_content, thinking_blocks, content)`
  returns `(reasoning_text, cleaned_content)` with a defined fallback
  order: dedicated field → Anthropic thinking_blocks → inline
  `<think>`/`<thought>` tags. Models that expose none of these
  short-circuit to `(None, content)` — zero overhead.
- `IncrementalThinkExtractor` replaces the ad-hoc `emit_incremental_think`
  function and its hand-rolled "emitted cursor" state in both the loop
  hook and the runner progress stream.

Also documented the new `showReasoning` channel option in
docs/configuration.md and noted its independence from sendProgress.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 17:14:19 +00:00
07f9ab580a fix(provider): preserve Bedrock tool config for history
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:59:01 +08:00
35f64cd828 docs(config): document model presets
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
079b37aac5 test(config): cover legacy model defaults without presets
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
13eede5803 refactor(agent): inject runtime model publisher
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
6554c1f832 refactor(agent): move preset helpers out of loop
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
e6103d9312 fix(agent): separate preset snapshots from config reload
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
8fcb24bb7c refactor(agent): trim model preset runtime wiring
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
70b8daaee6 fix(command): show default as current model preset
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
c9b84c7b11 fix(config): reserve implicit default model preset
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
1d14c2ba40 fix(config): accept modelPresets root alias
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
bcc4b97183 fix(webui): broadcast runtime model updates
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
c92345bbb1 fix(webui): sync model badge after preset switch
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
b61c6304c3 fix(config): reconcile presets with settings reload
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
c450d6fd3f fix(config): make model preset switching atomic
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 20:06:22 +08:00
1175420339 test(feishu): cover topic isolation alias
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 11:51:25 +08:00
fd6887c274 test(providers): cover VolcEngine token parameter
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 11:35:52 +08:00
23312d683e fix(tools): isolate plugin runtime state
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 11:28:20 +08:00
6d07aa6059 test(webui): cover randomUUID entry shim fallback
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>
2026-05-11 15:39:05 +08:00
Xubin RenandCursor 4f1faea90c ci: optimize Test Suite workflow (safe subset)
Re-applies the safe portion of c01f8599 after the revert in 2e8e674e.
Drops the uv cache which broke last time because uv.lock is gitignored
in this repo, and keeps lint as a step inside the test job (matching
the pre-c01f8599 layout).

What's added (all metadata-only, no external dependencies):
- concurrency: cancel superseded runs on the same ref
- permissions: tighten GITHUB_TOKEN to contents: read
- timeout-minutes: 20 to bound runaway jobs
- fail-fast: false so all matrix combinations surface failures
- matrix conditional: PRs run Linux x {3.11, 3.14} for fast feedback;
  push to main/nightly still runs the full 2-OS x 4-Python matrix

What's intentionally NOT added (each removed for a reason):
- uv cache: depends on uv.lock which is gitignored
- separate lint job: kept inline as a step, matches original
- workflow_dispatch / paths-ignore: scope creep, not needed now

All jobs continue to run on standard GitHub-hosted runners
(ubuntu-latest, windows-latest), keeping CI within the free tier.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 08:27:46 +00:00
Xubin RenandCursor 2e8e674e38 revert(ci): restore original Test Suite workflow
The optimized workflow in c01f8599 set astral-sh/setup-uv@v4 with
cache-dependency-glob: "uv.lock", but uv.lock is gitignored in this
repo, so the hosted runner's checkout never contains it and the
Install uv step fails with:

  Error: No file matched to [uv.lock], make sure you have
  checked out the target repository

Reverting the workflow to the pre-c01f8599 version to unbreak CI.

The "Modifying CI Workflows" section added to CONTRIBUTING.md in the
same commit is left in place; it documents general guidance and is
independent of this specific implementation choice.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 08:22:09 +00:00
Xubin RenandCursor c01f85995f ci: optimize Test Suite workflow and document free-tier rule
Workflow changes (.github/workflows/ci.yml):
- Add concurrency to cancel superseded runs on the same ref
- Enable uv dependency caching keyed on uv.lock
- Split lint into a dedicated job; gate test on lint via needs
- Split matrix: PRs run Linux x {3.11, 3.14} for fast feedback;
  push to main/nightly still runs the full 2-OS x 4-Python matrix
- Add fail-fast: false so all platforms surface failures together
- Add timeouts (lint: 5m, test: 20m) to bound runaway jobs
- Tighten GITHUB_TOKEN to contents: read

Docs (CONTRIBUTING.md):
- Add a short "Modifying CI Workflows" section so contributors know
  to stay within standard runners / no metered storage / no paid
  actions before touching .github/workflows/

All jobs continue to run on standard GitHub-hosted runners
(ubuntu-latest, windows-latest), keeping CI within the free tier.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 08:15:27 +00: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
7c1aa5ae31 docs: refine AI contributor guidance
Clarify nanobot's preference for small core changes, reviewable PR boundaries, and careful handling of prompt/context surfaces so AI contributors preserve the project's maintenance philosophy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 14:00:32 +08:00
Xubin Ren 3231aaf9ee fix(image): prevent duplicate delivery and replay artifacts 2026-05-09 05:45:13 +00:00
bbdf1db30d fix(webui): render generated images as rounded previews
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 23:48:01 +08:00
151c3d5ad0 fix(webui): restore chat selection after settings
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 23:48:01 +08:00
2cc32ca07c feat(webui): redesign settings and BYOK configuration
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 23:48:01 +08:00
Xubin RenandCursor 451d740849 fix(webui): polish delete dialog and sidebar toggles
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 13:28:34 +00:00
cbd5b06075 fix(memory): align replay overflow with history trimming
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 20:37:03 +08:00
24daf9a51c test(memory): accept replay window in consolidation assertion
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 20:37:03 +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
2c830ca817 test(weixin): stabilize typing keepalive assertion
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 20:06:23 +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
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
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
Xubin RenandXubin Ren e54fbfeb2a test(cron): avoid Windows timer race
Disable the externally updated cron job before yielding to the event loop so slow Windows CI cannot run the short-interval job before the test writes the update.
2026-05-06 00:43:00 +08:00
db14685a69 fix(agent): soften SSRF guard recovery
Keep private URL access blocked at the tool boundary, but return a clear non-retryable hint so the agent can recover conversationally instead of aborting the turn.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 00:43:00 +08:00
4db50f2e32 fix(channels): reject unauthorized inbound before side effects
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-05 23:16:36 +08:00
1813fc5021 test(telegram): cover silent allowlist rejection
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-05 23:16:36 +08:00
614b21368f fix(agent): tighten safety guard edge cases
Keep the /dev workspace guard exception scoped to the known benign device paths already handled by ExecTool, and add coverage that non-benign /dev targets still get blocked. Also add a streaming regression for tool_error responses so fatal tool failures are delivered by channels instead of being marked as already streamed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 01:25:52 +08:00
2a7433b7ec chore(runner): tighten workspace guard comments and Windows tests
Keep the workspace-boundary changes easier to review by trimming long explanatory comments down to short local notes. Also make the #3599 POSIX command regression skip on Windows and normalize workspace violation signatures to POSIX separators so the throttle tests are platform-stable.

Tests:
- uv run pytest tests/tools/test_exec_security.py tests/utils/test_workspace_violation_throttle.py -q
- uv run pytest -q

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 01:18:39 +08:00
b8406be215 fix(runner): soft workspace boundary + per-target throttle (#3493 #3599 #3605)
Replaces PR #3493's blanket fatal abort with a "tell the model + throttle
the bypass loop" policy.  Workspace-bound rejections are now ordinary
recoverable tool errors enriched with a structured "this is a hard policy
boundary" instruction; SSRF stays the only marker that aborts the turn.

Why the fatal-abort approach broke
----------------------------------
PR #3493 promoted every shell `_guard_command` and filesystem path-resolution
rejection to a turn-fatal RuntimeError.  Two of those messages (`path
outside working dir` and `path traversal detected`) are heuristic substring
scans on the raw command, so legitimate commands like `rm <ws>/x.txt
2>/dev/null` or `find . -type f` killed the user's turn (#3599).  On
channels with outbound dedupe (Telegram) the user just saw silence (#3605),
and the noise polluted the LLM's context until it started hallucinating
guard rejections on plain relative paths (#3597).

Why we still need *some* throttle
---------------------------------
The original #3493 pain point was real: the LLM, refused once, would
swap tools and try again -- read_file -> exec cat -> exec cp -> bash -c
-> ln -sf -> python -c open(...).  Just removing the fatal escape lets
that loop run wild until max_iterations.

What this commit does
---------------------
- `nanobot/utils/runtime.py`: add `workspace_violation_signature` and
  `repeated_workspace_violation_error`.  The signature normalizes
  filesystem `path` arguments and the first absolute path inside an
  exec command, so swapping tools against the same outside target hits
  the same throttle bucket.  Two soft attempts are allowed; the third
  attempt's tool result is replaced with a hard "stop trying to bypass"
  message that quotes the target path and tells the model to ask the
  user for help.

- `nanobot/agent/runner.py`: split classification into `_is_ssrf_violation`
  (still fatal) and `_is_workspace_violation` (now soft).  All three
  failure branches in `_run_tool` (prep_error / exception / Error
  result) route through a shared `_classify_violation` that bumps the
  per-turn workspace_violation_counts dict and either keeps the tool's
  own message or substitutes the throttle escalation.  `_execute_tools`
  now threads that dict alongside the existing external_lookup_counts.

- `nanobot/agent/tools/shell.py`: append a structured boundary note to
  every workspace-bound guard rejection (`working_dir could not be
  resolved`, `working_dir is outside`, `path outside working dir`,
  `path traversal detected`).  SSRF errors stay short and direct so the
  model doesn't try to "phrase around" them.  Existing `2>/dev/null`
  allow-list and benign device passthrough from the previous commit
  remain.

- `nanobot/agent/tools/filesystem.py`: append the same boundary note to
  the `outside allowed directory` PermissionError so read_file / write_file
  / list_dir errors give the LLM the same explicit hint.

Tests
-----
- `tests/utils/test_workspace_violation_throttle.py` (new): signature
  collapses across read_file/exec/python -c against the same path,
  different paths get independent budgets, escalation only fires after
  the third attempt.

- `tests/agent/test_runner.py`:
  - `test_runner_does_not_abort_on_workspace_violation_anymore` -- v2
    contract: filesystem PermissionError is now soft, runner moves to
    the next iteration and finalizes cleanly.
  - `test_is_ssrf_violation_remains_fatal` + the existing
    `test_runner_aborts_on_ssrf_violation` -- SSRF still aborts on the
    first attempt.
  - `test_runner_lets_llm_recover_from_shell_guard_path_outside` -- end
    to end recovery from `path outside working dir`.
  - `test_runner_throttles_repeated_workspace_bypass_attempts` -- four
    bypass attempts against the same outside target produce at least
    one `workspace_violation_escalated` event and the run completes
    naturally without aborting the turn.
  - The two `_execute_tools` direct-call tests now pass the new
    workspace_violation_counts dict.

- `tests/tools/test_tool_validation.py`: relax three `==` assertions
  to `startswith` + "hard policy boundary" substring check to match
  the new structured error messages.

- `tests/tools/test_exec_security.py` keeps the prior `2>/dev/null`
  regression and the `> /etc/issue` negative case from the previous
  commit on this branch -- they still pass under the new policy.

Coverage status: full pytest 2648 passed / 2 skipped (was 2638 / 2
on origin/main).  Ruff is clean for every file touched in this commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 01:18:39 +08:00
Xubin RenandXubin Ren 7742f8fbdc fix(runner): narrow workspace_violation fatal classification (#3599, helps #3605 #3597)
PR #3493 promoted every shell `_guard_command` rejection to a turn-fatal
RuntimeError. The two heuristic outputs in that list -- `path outside
working dir` and `path traversal detected` -- routinely false-positive on
benign constructs (e.g. `2>/dev/null`, quoted `..` arguments to sed/find,
absolute paths inside inline scripts), so legitimate workspace commands
silently kill the user's turn (#3599) and the agent never gets a chance
to retry with a different approach (#3605).

Two changes, both narrowly scoped:

- `ExecTool._guard_command` now skips a small allow-list of kernel device
  files (`/dev/null`, the standard streams, `/dev/random`, `/dev/fd/N`,
  ...) before the workspace path check, matched against the pre-resolve
  string so symlinks like `/dev/stderr -> /proc/self/fd/2` still hit the
  allow-list. Real outside writes such as `> /etc/issue` remain blocked.
- `AgentRunner._WORKSPACE_BLOCK_MARKERS` keeps only the four hard
  path-resolution errors from filesystem.py / shell.py and the SSRF
  marker. The two heuristic substrings move out of the fatal list, so
  the LLM sees them as ordinary tool errors and can self-correct in the
  next iteration. SSRF stays fatal because retrying an internal URL
  with a different phrasing would defeat the safety boundary.

Tests:
- `tests/tools/test_exec_security.py`: parametrized regression for the
  exact #3599 command sample plus other stdio redirects and device
  reads; explicit negative case asserts `> /etc/issue` is still blocked.
- `tests/agent/test_runner.py`: `_is_workspace_violation` no longer
  fatals on the two heuristic markers, plus an end-to-end case proving
  the runner hands the guard error back to the LLM and finalizes the
  next turn cleanly.
2026-05-04 01:18:39 +08:00
9a9e446f3f fix(cron): clean persistence lint issues
Keep the cron persistence hardening clean under ruff without changing behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 00:16:39 +08:00
66682eb46f test(cli): cover retry-wait interactive routing
Keep provider retry wait messages on the interactive progress path so they do not fall through as assistant responses.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 22:59:08 +08:00
7faa339902 fix(webui): keep existing package lockfile
Restore the npm lockfile that is already present on main so this PR only carries the WebUI turn-completion changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 22:28:40 +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
Xubin Ren 2fa15ccf1b fix: improve media failure diagnostics and token fallback coverage 2026-05-02 11:37:07 +00:00
Xubin Ren fde530de01 refactor(setup): enhance SKILL.md for upgrade process clarity 2026-05-02 07:40:29 +00:00
861fbb0dde fix(provider): correct LongCat OpenAI base URL
Use the SDK-ready /v1 base so LongCat chat completions hit the documented endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-02 01:52:04 +08:00
fd1a5a6267 test(provider): tidy Anthropic fallback imports
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 23:59:24 +08:00
Xubin RenandXubin Ren 539d82eadc test(tools): accept spawn origin message context
Made-with: Cursor
2026-05-01 20:09:59 +08:00
Xubin RenandXubin Ren 188e6df757 fix(utils): cover complete trailing think markers
Made-with: Cursor
2026-05-01 20:09:59 +08:00
Xubin RenandXubin Ren aea5948b11 fix(tools): tighten web fetch URL cleaning
Made-with: Cursor
2026-05-01 19:58:19 +08:00
Xubin RenandXubin Ren 43a58335f6 fix(provider): narrow DeepSeek reasoning history cleanup
Made-with: Cursor
2026-05-01 19:52:38 +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
Xubin Ren 1c24f10236 fix(skills): update restart instructions in upgrade process 2026-05-01 11:18:47 +00: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
Xubin RenandXubin Ren 6891a7a4d4 fix(skills): correct update setup commands
Made-with: Cursor
2026-05-01 19:02:26 +08:00
Xubin RenandXubin Ren 306958d6e6 add native Bedrock Converse provider
Made-with: Cursor
2026-05-01 18:52:03 +08:00
Xubin RenandXubin Ren f8fd9f0011 fix(feishu): keep streaming replies in existing topics
Made-with: Cursor
2026-04-30 13:42:37 +08:00
Xubin Ren 71eff09653 fix(whatsapp): refresh bridge when source changes 2026-04-30 04:18:31 +00:00
Xubin Ren d23bcae5a3 chore: update README with news for v0.1.5.post4 release 2026-04-29 11:12:50 +00:00
Xubin Ren 69bcf26ef4 chore: update README with news for v0.1.5.post3 release 2026-04-29 10:59:19 +00:00
Xubin Ren 0b1631f33d chore: bump version to 0.1.5.post3 and update README news
- pyproject.toml + __init__.py: 0.1.5.post2 → 0.1.5.post3
- README: add daily news entries for 2026-04-22 through 2026-04-28

Made-with: Cursor
2026-04-29 10:50:57 +00:00
Xubin RenandXubin Ren 3d7099b421 fix(memory): clean atomic write test hygiene
Made-with: Cursor
2026-04-29 16:57:50 +08:00
Xubin Ren 67b4d113c9 chore: update pyproject.toml 2026-04-29 08:25:09 +00:00
Xubin RenandXubin Ren 278ef22776 docs(config): document provider extra body
Show how to configure OpenAI-compatible request body extensions such as sampling and chat template parameters.

Made-with: Cursor
2026-04-28 15:56:13 +08:00
Xubin Ren f4d8783f5e test(web): cover configurable fetch behavior
Ensure custom user agents are applied to direct web requests and disabling Jina Reader forces the local readability path.

Made-with: Cursor
2026-04-28 07:25:47 +00:00
Xubin Ren 18432c313f Merge origin/main into web-tools
Made-with: Cursor
2026-04-28 07:17:05 +00:00
Xubin RenandXubin Ren 50698c3d1c test(telegram): cover local attachment filenames
Add a regression test for preserving the original basename when Telegram sends local media bytes.

Made-with: Cursor
2026-04-28 15:13:49 +08:00