feat: add image generation tool and WebUI mode

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xubin Ren
2026-05-08 20:06:23 +08:00
committed by Xubin Ren
co-authored by Cursor
parent 3a2f47d720
commit e936ed48bd
45 changed files with 2979 additions and 89 deletions
+35 -3
View File
@@ -30,6 +30,7 @@ from nanobot.agent.tools.ask import (
from nanobot.agent.tools.cron import CronTool
from nanobot.agent.tools.file_state import FileStateStore, bind_file_states, reset_file_states
from nanobot.agent.tools.filesystem import EditFileTool, ListDirTool, ReadFileTool, WriteFileTool
from nanobot.agent.tools.image_generation import ImageGenerationTool
from nanobot.agent.tools.message import MessageTool
from nanobot.agent.tools.notebook import NotebookEditTool
from nanobot.agent.tools.registry import ToolRegistry
@@ -45,9 +46,11 @@ from nanobot.config.schema import AgentDefaults
from nanobot.providers.base import LLMProvider
from nanobot.providers.factory import ProviderSnapshot
from nanobot.session.manager import Session, SessionManager
from nanobot.utils.artifacts import generated_image_paths_from_messages
from nanobot.utils.document import extract_documents
from nanobot.utils.helpers import image_placeholder_text
from nanobot.utils.helpers import truncate_text as truncate_text_fn
from nanobot.utils.image_generation_intent import image_generation_prompt
from nanobot.utils.progress_events import (
build_tool_event_finish_payloads,
build_tool_event_start_payload,
@@ -58,7 +61,13 @@ from nanobot.utils.runtime import EMPTY_FINAL_RESPONSE_MESSAGE
from nanobot.utils.webui_titles import mark_webui_session, maybe_generate_webui_title_after_turn
if TYPE_CHECKING:
from nanobot.config.schema import ChannelsConfig, ExecToolConfig, ToolsConfig, WebToolsConfig
from nanobot.config.schema import (
ChannelsConfig,
ExecToolConfig,
ProviderConfig,
ToolsConfig,
WebToolsConfig,
)
from nanobot.cron.service import CronService
@@ -215,6 +224,8 @@ class AgentLoop:
unified_session: bool = False,
disabled_skills: list[str] | None = None,
tools_config: ToolsConfig | None = None,
image_generation_provider_config: ProviderConfig | None = None,
image_generation_provider_configs: dict[str, ProviderConfig] | None = None,
provider_snapshot_loader: Callable[[], ProviderSnapshot] | None = None,
provider_signature: tuple[object, ...] | None = None,
):
@@ -250,6 +261,13 @@ class AgentLoop:
)
self.web_config = web_config or WebToolsConfig()
self.exec_config = exec_config or ExecToolConfig()
self.tools_config = _tc
self._image_generation_provider_configs = dict(image_generation_provider_configs or {})
if (
image_generation_provider_config is not None
and "openrouter" not in self._image_generation_provider_configs
):
self._image_generation_provider_configs["openrouter"] = image_generation_provider_config
self.cron_service = cron_service
self.restrict_to_workspace = restrict_to_workspace
self._start_time = time.time()
@@ -404,6 +422,14 @@ class AgentLoop:
user_agent=self.web_config.user_agent,
)
)
if self.tools_config.image_generation.enabled:
self.tools.register(
ImageGenerationTool(
workspace=self.workspace,
config=self.tools_config.image_generation,
provider_configs=self._image_generation_provider_configs,
)
)
self.tools.register(MessageTool(send_callback=self.bus.publish_outbound, workspace=self.workspace))
self.tools.register(SpawnTool(manager=self.subagents))
if self.cron_service:
@@ -1063,12 +1089,12 @@ class AgentLoop:
self.context.build_system_prompt(channel=msg.channel),
history,
pending_ask_id,
msg.content,
image_generation_prompt(msg.content, msg.metadata),
)
else:
initial_messages = self.context.build_messages(
history=history,
current_message=msg.content,
current_message=image_generation_prompt(msg.content, msg.metadata),
session_summary=pending,
media=msg.media if msg.media else None,
channel=msg.channel,
@@ -1143,6 +1169,11 @@ class AgentLoop:
# Skip the already-persisted user message when saving the turn
save_skip = 1 + len(history) + (1 if user_persisted_early else 0)
generated_media = generated_image_paths_from_messages(all_msgs[save_skip:])
if generated_media and all_msgs and all_msgs[-1].get("role") == "assistant":
existing_media = all_msgs[-1].get("media")
media = existing_media if isinstance(existing_media, list) else []
all_msgs[-1]["media"] = list(dict.fromkeys([*media, *generated_media]))
self._save_turn(session, all_msgs, save_skip)
session.enforce_file_cap(on_archive=self.context.memory.raw_archive)
self._clear_pending_user_turn(session)
@@ -1175,6 +1206,7 @@ class AgentLoop:
channel=msg.channel,
chat_id=msg.chat_id,
content=final_content,
media=generated_media,
metadata=meta,
buttons=buttons,
)