Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bf430e6
feat: Introduce cron job management and refactor tool provisioning wi…
advent259141 Mar 10, 2026
ff4412a
refactor: Centralize and decouple computer-use tool injection logic i…
advent259141 Mar 10, 2026
21f1fa8
feat: Implement API routes and dashboard UI for managing tools and MC…
advent259141 Mar 10, 2026
42b8293
Merge branch 'AstrBotDevs:master' into agent-fix-clean
advent259141 Mar 11, 2026
894d72e
feat: Introduce an internal agent sub-stage to the pipeline, enabling…
advent259141 Mar 11, 2026
eae87e1
Merge branch 'agent-fix-clean' of https://github.com/advent259141/Ast…
advent259141 Mar 11, 2026
438fc10
feat: 增加在工具注入前对工具是否启用的检查
advent259141 Mar 11, 2026
f16edd4
refactor: delegate tool injection to booter self-description API
w31r4 Mar 11, 2026
e85eef0
fix: stabilize tool injection for LLM prefix cache hits
w31r4 Mar 11, 2026
3440dcd
test: add booter decoupling and profile-aware tool tests
w31r4 Mar 10, 2026
ad3911a
refactor: add debug logging to sandbox tool resolution
w31r4 Mar 10, 2026
e1d7611
refactor: standardize booter structured logging format
w31r4 Mar 10, 2026
a5a1ba7
refactor: add get_sandbox_capabilities API and structured logging to …
w31r4 Mar 10, 2026
7c3cc7b
refactor: add capabilities to sandbox tool binding logs
w31r4 Mar 10, 2026
dfc0c34
fix: address review issues in tool injection refactor
w31r4 Mar 12, 2026
048c511
fix: align browser property with base class and remove dead env writes
w31r4 Mar 12, 2026
855483c
style: fix ruff I001/F401 violations in changed files
w31r4 Mar 12, 2026
c07fba7
merge: resolve conflicts with origin/master
advent259141 Mar 13, 2026
3a8bfa0
style: ruff format on merge-touched files
advent259141 Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ GenieData/
.codex/
.opencode/
.kilocode/
.serena
.worktrees/

1 change: 1 addition & 0 deletions astrbot/core/agent/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ def __init__(
self.mcp_tool = mcp_tool
self.mcp_client = mcp_client
self.mcp_server_name = mcp_server_name
self.source = "mcp"

async def call(
self, context: ContextWrapper[TContext], **kwargs
Expand Down
14 changes: 14 additions & 0 deletions astrbot/core/agent/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ class FunctionTool(ToolSchema, Generic[TContext]):
Declare this tool as a background task. Background tasks return immediately
with a task identifier while the real work continues asynchronously.
"""
source: str = "plugin"
"""
Origin of this tool: 'plugin' (from star plugins), 'internal' (AstrBot built-in),
or 'mcp' (from MCP servers). Used by WebUI for display grouping.
"""

def __repr__(self) -> str:
return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
Expand Down Expand Up @@ -101,6 +106,15 @@ def remove_tool(self, name: str) -> None:
"""Remove a tool by its name."""
self.tools = [tool for tool in self.tools if tool.name != name]

def normalize(self) -> None:
"""Sort tools by name for deterministic serialization.

This ensures the serialized tool schema sent to the LLM is
identical across requests regardless of registration/injection
order, enabling LLM provider prefix cache hits.
"""
self.tools.sort(key=lambda t: t.name)

def get_tool(self, name: str) -> FunctionTool | None:
"""Get a tool by its name."""
for tool in self.tools:
Expand Down
135 changes: 97 additions & 38 deletions astrbot/core/astr_agent_tool_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@
from astrbot.core.agent.tool import FunctionTool, ToolSet
from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
from astrbot.core.astr_agent_context import AstrAgentContext
from astrbot.core.astr_main_agent_resources import (
BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT,
EXECUTE_SHELL_TOOL,
FILE_DOWNLOAD_TOOL,
FILE_UPLOAD_TOOL,
LOCAL_EXECUTE_SHELL_TOOL,
LOCAL_PYTHON_TOOL,
PYTHON_TOOL,
SEND_MESSAGE_TO_USER_TOOL,
)
from astrbot.core.cron.events import CronMessageEvent
from astrbot.core.message.components import Image
from astrbot.core.message.message_event_result import (
Expand All @@ -37,6 +27,12 @@
from astrbot.core.platform.message_session import MessageSession
from astrbot.core.provider.entites import ProviderRequest
from astrbot.core.provider.register import llm_tools
from astrbot.core.tools.prompts import (
BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT,
BACKGROUND_TASK_WOKE_USER_PROMPT,
CONVERSATION_HISTORY_INJECT_PREFIX,
)
from astrbot.core.tools.send_message import SEND_MESSAGE_TO_USER_TOOL
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
from astrbot.core.utils.history_saver import persist_agent_history
from astrbot.core.utils.image_ref_utils import is_supported_image_ref
Expand Down Expand Up @@ -172,25 +168,90 @@ async def _run_in_background() -> None:

return
else:
# Guard: reject sandbox tools whose capability is unavailable.
# Tools are always injected (for schema stability / prefix caching),
# but execution is blocked when the sandbox lacks the capability.
rejection = cls._check_sandbox_capability(tool, run_context)
if rejection is not None:
yield rejection
return

async for r in cls._execute_local(tool, run_context, **tool_args):
yield r
return

# Browser tool names that require the "browser" sandbox capability.
_BROWSER_TOOL_NAMES: frozenset[str] = frozenset(
{
"astrbot_execute_browser",
"astrbot_execute_browser_batch",
"astrbot_run_browser_skill",
}
)

@classmethod
def _check_sandbox_capability(
cls,
tool: FunctionTool,
run_context: ContextWrapper[AstrAgentContext],
) -> mcp.types.CallToolResult | None:
"""Return a rejection result if the tool requires a sandbox capability
that is not available, or None if the tool may proceed."""
if tool.name not in cls._BROWSER_TOOL_NAMES:
return None

from astrbot.core.computer.computer_client import get_sandbox_capabilities

session_id = run_context.context.event.unified_msg_origin
caps = get_sandbox_capabilities(session_id)

# Sandbox not yet booted — allow through (boot will happen on first
# shell/python call; browser tools will fail naturally if truly unavailable).
if caps is None:
return None

if "browser" not in caps:
msg = (
f"Tool '{tool.name}' requires browser capability, but the current "
f"sandbox profile does not include it (capabilities: {list(caps)}). "
"Please ask the administrator to switch to a sandbox profile with "
"browser support, or use shell/python tools instead."
)
logger.warning(
"[ToolExec] capability_rejected tool=%s caps=%s", tool.name, list(caps)
)
return mcp.types.CallToolResult(
content=[mcp.types.TextContent(type="text", text=msg)],
isError=True,
)

return None

@classmethod
def _get_runtime_computer_tools(cls, runtime: str) -> dict[str, FunctionTool]:
if runtime == "sandbox":
return {
EXECUTE_SHELL_TOOL.name: EXECUTE_SHELL_TOOL,
PYTHON_TOOL.name: PYTHON_TOOL,
FILE_UPLOAD_TOOL.name: FILE_UPLOAD_TOOL,
FILE_DOWNLOAD_TOOL.name: FILE_DOWNLOAD_TOOL,
}
if runtime == "local":
return {
LOCAL_EXECUTE_SHELL_TOOL.name: LOCAL_EXECUTE_SHELL_TOOL,
LOCAL_PYTHON_TOOL.name: LOCAL_PYTHON_TOOL,
}
return {}
def _get_runtime_computer_tools(
cls,
runtime: str,
sandbox_cfg: dict | None = None,
session_id: str = "",
) -> dict[str, FunctionTool]:
from astrbot.core.computer.computer_tool_provider import ComputerToolProvider
from astrbot.core.tool_provider import ToolProviderContext

provider = ComputerToolProvider()
ctx = ToolProviderContext(
computer_use_runtime=runtime,
sandbox_cfg=sandbox_cfg,
session_id=session_id,
)
tools = provider.get_tools(ctx)
result = {tool.name: tool for tool in tools}
logger.info(
"[Computer] sandbox_tool_binding target=subagent runtime=%s tools=%d session=%s",
runtime,
len(result),
session_id,
)
return result

@classmethod
def _build_handoff_toolset(
Expand All @@ -203,7 +264,12 @@ def _build_handoff_toolset(
cfg = ctx.get_config(umo=event.unified_msg_origin)
provider_settings = cfg.get("provider_settings", {})
runtime = str(provider_settings.get("computer_use_runtime", "local"))
runtime_computer_tools = cls._get_runtime_computer_tools(runtime)
sandbox_cfg = provider_settings.get("sandbox", {})
runtime_computer_tools = cls._get_runtime_computer_tools(
runtime,
sandbox_cfg=sandbox_cfg,
session_id=event.unified_msg_origin,
)

# Keep persona semantics aligned with the main agent: tools=None means
# "all tools", including runtime computer-use tools.
Expand Down Expand Up @@ -346,7 +412,7 @@ async def _run_handoff_in_background() -> None:
type="text",
text=(
f"Background task dedicated to subagent '{tool.agent.name}' submitted. task_id={task_id}. "
f"The subagent '{tool.agent.name}' is working on the task on hehalf you. "
f"The subagent '{tool.agent.name}' is working on the task on behalf of you. "
f"You will be notified when it finishes."
),
)
Expand Down Expand Up @@ -480,11 +546,14 @@ async def _wake_main_agent_for_background_result(
message_type=session.message_type,
)
cron_event.role = event.role
from astrbot.core.computer.computer_tool_provider import ComputerToolProvider

config = MainAgentBuildConfig(
tool_call_timeout=3600,
streaming_response=ctx.get_config()
.get("provider_settings", {})
.get("stream", False),
tool_providers=[ComputerToolProvider()],
)

req = ProviderRequest()
Expand All @@ -495,23 +564,13 @@ async def _wake_main_agent_for_background_result(
req.contexts = context
context_dump = req._print_friendly_context()
req.contexts = []
req.system_prompt += (
"\n\nBellow is you and user previous conversation history:\n"
f"{context_dump}"
)
req.system_prompt += CONVERSATION_HISTORY_INJECT_PREFIX + context_dump

bg = json.dumps(extras["background_task_result"], ensure_ascii=False)
req.system_prompt += BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT.format(
background_task_result=bg
)
req.prompt = (
"Proceed according to your system instructions. "
"Output using same language as previous conversation. "
"If you need to deliver the result to the user immediately, "
"you MUST use `send_message_to_user` tool to send the message directly to the user, "
"otherwise the user will not see the result. "
"After completing your task, summarize and output your actions and results. "
)
req.prompt = BACKGROUND_TASK_WOKE_USER_PROMPT
if not req.func_tool:
req.func_tool = ToolSet()
req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL)
Expand Down
Loading