Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions astrbot/core/agent/runners/tool_loop_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ async def reset(
request_max_retries: int | None = None,
tool_result_overflow_dir: str | None = None,
read_tool: FunctionTool | None = None,
overflow_file_writer: T.Callable[[str, str], T.Awaitable[str]] | None = None,
**kwargs: T.Any,
) -> None:
self.req = request
Expand All @@ -241,6 +242,7 @@ async def reset(
self.request_max_retries = request_max_retries
self.tool_result_overflow_dir = tool_result_overflow_dir
self.read_tool = read_tool
self._overflow_file_writer = overflow_file_writer
self._tool_result_token_counter = EstimateTokenCounter()
self.request_context_manager_config = ContextConfig(
# <=0 disables token-based guarding.
Expand Down Expand Up @@ -369,6 +371,9 @@ async def _write_tool_result_overflow_file(
tool_call_id: str,
content: str,
) -> str:
if self._overflow_file_writer is not None:
return await self._overflow_file_writer(content, tool_call_id)

if self.tool_result_overflow_dir is None:
raise ValueError("tool_result_overflow_dir is not configured")

Expand Down
13 changes: 13 additions & 0 deletions astrbot/core/astr_main_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1533,6 +1533,18 @@ async def build_main_agent(
elif config.computer_use_runtime == "local":
_apply_local_env_tools(req, plugin_context)

overflow_file_writer = None
if (
config.computer_use_runtime == "sandbox"
and req.func_tool
and req.func_tool.get_tool("astrbot_file_read_tool")
):
from astrbot.core.computer.computer_client import make_sandbox_overflow_writer

overflow_file_writer = make_sandbox_overflow_writer(
plugin_context, event.unified_msg_origin
)

agent_runner = AgentRunner()
astr_agent_ctx = AstrAgentContext(
context=plugin_context,
Expand Down Expand Up @@ -1625,6 +1637,7 @@ async def build_main_agent(
read_tool=(
req.func_tool.get_tool("astrbot_file_read_tool") if req.func_tool else None
),
overflow_file_writer=overflow_file_writer,
)

if apply_reset:
Expand Down
34 changes: 34 additions & 0 deletions astrbot/core/computer/computer_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,40 @@ async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None:
logger.warning(f"Failed to remove temp skills zip: {zip_path}")


def make_sandbox_overflow_writer(
context: Context,
unified_msg_origin: str,
):
"""Build a callback that writes tool-result overflow content directly into the sandbox.

The returned callable has the signature
``(content: str, tool_call_id: str) -> Awaitable[str]`` and returns a
sandbox-relative path that ``astrbot_file_read_tool`` can resolve inside
the sandbox container.

Bay's filesystem API requires relative paths, so we write to a file under
the sandbox working directory rather than an absolute ``/tmp/...`` path.
"""

async def _write(content: str, tool_call_id: str) -> str:
safe_id = (
"".join(
ch if ch.isalnum() or ch in {"-", "_", "."} else "_"
for ch in tool_call_id
).strip("._")
or "tool_call"
)
sandbox_path = f"astrbot_overflow_{safe_id}_{uuid.uuid4().hex[:8]}.txt"
booter = await get_booter(context, unified_msg_origin)
Comment on lines +557 to +566

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider truncating the sanitized tool_call_id to avoid excessively long filenames in the sandbox.

If tool_call_id can be very long, safe_id may create filenames that exceed filesystem limits or be unwieldy in logs. Consider truncating safe_id (e.g., to 32–64 chars) before appending the UUID so filenames stay within reasonable bounds while remaining debuggable.

Suggested change
async def _write(content: str, tool_call_id: str) -> str:
safe_id = (
"".join(
ch if ch.isalnum() or ch in {"-", "_", "."} else "_"
for ch in tool_call_id
).strip("._")
or "tool_call"
)
sandbox_path = f"astrbot_overflow_{safe_id}_{uuid.uuid4().hex[:8]}.txt"
booter = await get_booter(context, unified_msg_origin)
async def _write(content: str, tool_call_id: str) -> str:
safe_id = (
"".join(
ch if ch.isalnum() or ch in {"-", "_", "."} else "_"
for ch in tool_call_id
).strip("._")
or "tool_call"
)
max_safe_id_len = 64
if len(safe_id) > max_safe_id_len:
safe_id = safe_id[:max_safe_id_len]
sandbox_path = f"astrbot_overflow_{safe_id}_{uuid.uuid4().hex[:8]}.txt"
booter = await get_booter(context, unified_msg_origin)

await booter.fs.write_file(sandbox_path, content)
logger.debug(
"[Computer] Overflow file written to sandbox: %s", sandbox_path
)
return sandbox_path

return _write


async def get_booter(
context: Context,
session_id: str,
Expand Down
16 changes: 16 additions & 0 deletions astrbot/core/star/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,15 @@ async def tool_loop_agent(
other_kwargs.setdefault(
"read_tool", request.func_tool.get_tool("astrbot_file_read_tool")
)
if self._is_sandbox_runtime(event.unified_msg_origin):
from astrbot.core.computer.computer_client import (
make_sandbox_overflow_writer,
)

other_kwargs.setdefault(
"overflow_file_writer",
make_sandbox_overflow_writer(self, event.unified_msg_origin),
)

await agent_runner.reset(
provider=prov,
Expand Down Expand Up @@ -503,6 +512,13 @@ def get_config(self, umo: str | None = None) -> AstrBotConfig:
return self._config
return self.astrbot_config_mgr.get_conf(umo)

def _is_sandbox_runtime(self, umo: str) -> bool:
cfg = self.get_config(umo=umo)
runtime = str(
cfg.get("provider_settings", {}).get("computer_use_runtime", "local")
)
Comment on lines +516 to +519

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

如果配置中的 provider_settings 显式为 Nonecfg.get("provider_settings", {}) 将返回 None,随后调用 .get() 会抛出 AttributeError。建议使用 cfg.get("provider_settings") or {} 进行防御性保护。

        cfg = self.get_config(umo=umo)
        provider_settings = cfg.get("provider_settings") or {}
        runtime = str(provider_settings.get("computer_use_runtime", "local"))

return runtime == "sandbox"

async def send_message(
self,
session: str | MessageSesion,
Expand Down
Loading