Skip to content

Commit a9dd635

Browse files
authored
Merge branch 'master' into master
2 parents cea5611 + b801003 commit a9dd635

83 files changed

Lines changed: 5554 additions & 990 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
1616
curl \
1717
gnupg \
1818
git \
19+
ripgrep \
1920
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
2021
&& apt-get install -y --no-install-recommends nodejs \
2122
&& apt-get clean \

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
1+
![astrbot-github-banner-v2-light-0405_副本](https://github.com/user-attachments/assets/36fb04e4-cc75-4454-bd8b-049d11aa86f9)
2+
23

34
<div align="center">
45

astrbot/cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.22.3"
1+
__version__ = "4.23.0-beta.1"

astrbot/core/agent/runners/tool_loop_agent_runner.py

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import time
55
import traceback
66
import typing as T
7+
import uuid
78
from collections.abc import AsyncIterator
89
from contextlib import suppress
910
from dataclasses import dataclass, field
11+
from pathlib import Path
1012

1113
from mcp.types import (
1214
BlobResourceContents,
@@ -25,7 +27,7 @@
2527

2628
from astrbot import logger
2729
from astrbot.core.agent.message import ImageURLPart, TextPart, ThinkPart
28-
from astrbot.core.agent.tool import ToolSet
30+
from astrbot.core.agent.tool import FunctionTool, ToolSet
2931
from astrbot.core.agent.tool_image_cache import tool_image_cache
3032
from astrbot.core.exceptions import EmptyModelOutputError
3133
from astrbot.core.message.components import Json
@@ -45,7 +47,7 @@
4547
from ..context.compressor import ContextCompressor
4648
from ..context.config import ContextConfig
4749
from ..context.manager import ContextManager
48-
from ..context.token_counter import TokenCounter
50+
from ..context.token_counter import EstimateTokenCounter, TokenCounter
4951
from ..hooks import BaseAgentRunHooks
5052
from ..message import AssistantMessageSegment, Message, ToolCallMessageSegment
5153
from ..response import AgentResponseData, AgentStats
@@ -97,6 +99,8 @@ class _ToolExecutionInterrupted(Exception):
9799

98100

99101
class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
102+
TOOL_RESULT_MAX_ESTIMATED_TOKENS = 27_500
103+
TOOL_RESULT_PREVIEW_MAX_ESTIMATED_TOKENS = 7000
100104
EMPTY_OUTPUT_RETRY_ATTEMPTS = 3
101105
EMPTY_OUTPUT_RETRY_WAIT_MIN_S = 1
102106
EMPTY_OUTPUT_RETRY_WAIT_MAX_S = 4
@@ -151,6 +155,12 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
151155
"Otherwise, change strategy, adjust arguments, or explain the limitation "
152156
"to the user."
153157
)
158+
TOOL_RESULT_OVERFLOW_NOTICE_TEMPLATE = (
159+
"Truncated tool output preview shown above. "
160+
"The tool output was too large to include directly and was written to "
161+
"`{overflow_path}`. Use {read_tool_hint} to inspect it. "
162+
"Use a narrower window when reading large files."
163+
)
154164

155165
def _get_persona_custom_error_message(self) -> str | None:
156166
"""Read persona-level custom error message from event extras when available."""
@@ -206,6 +216,8 @@ async def reset(
206216
custom_compressor: ContextCompressor | None = None,
207217
tool_schema_mode: str | None = "full",
208218
fallback_providers: list[Provider] | None = None,
219+
tool_result_overflow_dir: str | None = None,
220+
read_tool: FunctionTool | None = None,
209221
**kwargs: T.Any,
210222
) -> None:
211223
self.req = request
@@ -217,6 +229,9 @@ async def reset(
217229
self.truncate_turns = truncate_turns
218230
self.custom_token_counter = custom_token_counter
219231
self.custom_compressor = custom_compressor
232+
self.tool_result_overflow_dir = tool_result_overflow_dir
233+
self.read_tool = read_tool
234+
self._tool_result_token_counter = EstimateTokenCounter()
220235
# we will do compress when:
221236
# 1. before requesting LLM
222237
# TODO: 2. after LLM output a tool call
@@ -298,6 +313,103 @@ async def reset(
298313
self.stats = AgentStats()
299314
self.stats.start_time = time.time()
300315

316+
def _read_tool_hint(self) -> str:
317+
if self.read_tool is not None:
318+
return f"`{self.read_tool.name}`"
319+
return "the available file-read tool"
320+
321+
async def _write_tool_result_overflow_file(
322+
self,
323+
*,
324+
tool_call_id: str,
325+
content: str,
326+
) -> str:
327+
if self.tool_result_overflow_dir is None:
328+
raise ValueError("tool_result_overflow_dir is not configured")
329+
330+
overflow_dir = Path(self.tool_result_overflow_dir).resolve(strict=False)
331+
safe_tool_call_id = (
332+
"".join(
333+
ch if ch.isalnum() or ch in {"-", "_", "."} else "_"
334+
for ch in tool_call_id
335+
).strip("._")
336+
or "tool_call"
337+
)
338+
file_name = f"{safe_tool_call_id}_{uuid.uuid4().hex[:8]}.txt"
339+
overflow_path = overflow_dir / file_name
340+
341+
def _run() -> str:
342+
overflow_dir.mkdir(parents=True, exist_ok=True)
343+
overflow_path.write_text(content, encoding="utf-8")
344+
return str(overflow_path)
345+
346+
return await asyncio.to_thread(_run)
347+
348+
async def _materialize_large_tool_result(
349+
self,
350+
*,
351+
tool_call_id: str,
352+
content: str,
353+
) -> str:
354+
if self.tool_result_overflow_dir is None or self.read_tool is None:
355+
return content
356+
357+
estimated_tokens = self._tool_result_token_counter.count_tokens(
358+
[Message(role="tool", content=content, tool_call_id=tool_call_id)]
359+
)
360+
if estimated_tokens <= self.TOOL_RESULT_MAX_ESTIMATED_TOKENS:
361+
return content
362+
363+
preview = self._truncate_tool_result_preview(content, tool_call_id=tool_call_id)
364+
try:
365+
overflow_path = await self._write_tool_result_overflow_file(
366+
tool_call_id=tool_call_id,
367+
content=content,
368+
)
369+
except Exception as exc:
370+
logger.warning(
371+
"Failed to spill oversized tool result for %s: %s",
372+
tool_call_id,
373+
exc,
374+
exc_info=True,
375+
)
376+
error_notice = (
377+
"Tool output exceeded the inline result limit "
378+
f"({estimated_tokens} estimated tokens > "
379+
f"{self.TOOL_RESULT_MAX_ESTIMATED_TOKENS}) and could not be written "
380+
f"to `{self.tool_result_overflow_dir}`: {exc}"
381+
)
382+
if not preview:
383+
return error_notice
384+
return f"{preview}\n\n{error_notice}"
385+
386+
notice = self.TOOL_RESULT_OVERFLOW_NOTICE_TEMPLATE.format(
387+
overflow_path=overflow_path,
388+
read_tool_hint=self._read_tool_hint(),
389+
)
390+
if not preview:
391+
return notice
392+
return f"{preview}\n\n{notice}"
393+
394+
def _truncate_tool_result_preview(
395+
self,
396+
content: str,
397+
*,
398+
tool_call_id: str,
399+
) -> str:
400+
preview = content
401+
while preview:
402+
estimated_tokens = self._tool_result_token_counter.count_tokens(
403+
[Message(role="tool", content=preview, tool_call_id=tool_call_id)]
404+
)
405+
if estimated_tokens <= self.TOOL_RESULT_PREVIEW_MAX_ESTIMATED_TOKENS:
406+
return preview
407+
next_len = len(preview) // 2
408+
if next_len <= 0:
409+
break
410+
preview = preview[:next_len]
411+
return preview
412+
301413
async def _iter_llm_responses(
302414
self, *, include_model: bool = True
303415
) -> T.AsyncGenerator[LLMResponse, None]:
@@ -1009,9 +1121,15 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
10091121
logger.warning(
10101122
f"[EnhancedSubAgent] Failed to add dynamic tool: {e}"
10111123
)
1124+
else:
1125+
inline_result = "\n\n".join(result_parts)
1126+
inline_result = await self._materialize_large_tool_result(
1127+
tool_call_id=func_tool_id,
1128+
content=inline_result,
1129+
)
10121130
_append_tool_call_result(
10131131
func_tool_id,
1014-
"\n\n".join(result_parts)
1132+
inline_result
10151133
+ self._build_repeated_tool_call_guidance(
10161134
func_tool_name, tool_call_streak
10171135
),

astrbot/core/astr_agent_tool_exec.py

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,6 @@
2020
from astrbot.core.astr_agent_context import AstrAgentContext
2121
from astrbot.core.astr_main_agent_resources import (
2222
BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT,
23-
EXECUTE_SHELL_TOOL,
24-
FILE_DOWNLOAD_TOOL,
25-
FILE_UPLOAD_TOOL,
26-
LOCAL_EXECUTE_SHELL_TOOL,
27-
LOCAL_PYTHON_TOOL,
28-
PYTHON_TOOL,
2923
)
3024
from astrbot.core.cron.events import CronMessageEvent
3125
from astrbot.core.dynamic_subagent_manager import DynamicSubAgentManager
@@ -38,6 +32,17 @@
3832
from astrbot.core.platform.message_session import MessageSession
3933
from astrbot.core.provider.entites import ProviderRequest
4034
from astrbot.core.provider.register import llm_tools
35+
from astrbot.core.tools.computer_tools import (
36+
ExecuteShellTool,
37+
FileDownloadTool,
38+
FileEditTool,
39+
FileReadTool,
40+
FileUploadTool,
41+
FileWriteTool,
42+
GrepTool,
43+
LocalPythonTool,
44+
PythonTool,
45+
)
4146
from astrbot.core.tools.message_tools import SendMessageToUserTool
4247
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
4348
from astrbot.core.utils.history_saver import persist_agent_history
@@ -179,18 +184,44 @@ async def _run_in_background() -> None:
179184
return
180185

181186
@classmethod
182-
def _get_runtime_computer_tools(cls, runtime: str) -> dict[str, FunctionTool]:
187+
def _get_runtime_computer_tools(
188+
cls,
189+
runtime: str,
190+
tool_mgr,
191+
) -> dict[str, FunctionTool]:
183192
if runtime == "sandbox":
193+
shell_tool = tool_mgr.get_builtin_tool(ExecuteShellTool)
194+
python_tool = tool_mgr.get_builtin_tool(PythonTool)
195+
upload_tool = tool_mgr.get_builtin_tool(FileUploadTool)
196+
download_tool = tool_mgr.get_builtin_tool(FileDownloadTool)
197+
read_tool = tool_mgr.get_builtin_tool(FileReadTool)
198+
write_tool = tool_mgr.get_builtin_tool(FileWriteTool)
199+
edit_tool = tool_mgr.get_builtin_tool(FileEditTool)
200+
grep_tool = tool_mgr.get_builtin_tool(GrepTool)
184201
return {
185-
EXECUTE_SHELL_TOOL.name: EXECUTE_SHELL_TOOL,
186-
PYTHON_TOOL.name: PYTHON_TOOL,
187-
FILE_UPLOAD_TOOL.name: FILE_UPLOAD_TOOL,
188-
FILE_DOWNLOAD_TOOL.name: FILE_DOWNLOAD_TOOL,
202+
shell_tool.name: shell_tool,
203+
python_tool.name: python_tool,
204+
upload_tool.name: upload_tool,
205+
download_tool.name: download_tool,
206+
read_tool.name: read_tool,
207+
write_tool.name: write_tool,
208+
edit_tool.name: edit_tool,
209+
grep_tool.name: grep_tool,
189210
}
190211
if runtime == "local":
212+
shell_tool = tool_mgr.get_builtin_tool(ExecuteShellTool)
213+
python_tool = tool_mgr.get_builtin_tool(LocalPythonTool)
214+
read_tool = tool_mgr.get_builtin_tool(FileReadTool)
215+
write_tool = tool_mgr.get_builtin_tool(FileWriteTool)
216+
edit_tool = tool_mgr.get_builtin_tool(FileEditTool)
217+
grep_tool = tool_mgr.get_builtin_tool(GrepTool)
191218
return {
192-
LOCAL_EXECUTE_SHELL_TOOL.name: LOCAL_EXECUTE_SHELL_TOOL,
193-
LOCAL_PYTHON_TOOL.name: LOCAL_PYTHON_TOOL,
219+
shell_tool.name: shell_tool,
220+
python_tool.name: python_tool,
221+
read_tool.name: read_tool,
222+
write_tool.name: write_tool,
223+
edit_tool.name: edit_tool,
224+
grep_tool.name: grep_tool,
194225
}
195226
return {}
196227

@@ -205,7 +236,15 @@ def _build_handoff_toolset(
205236
cfg = ctx.get_config(umo=event.unified_msg_origin)
206237
provider_settings = cfg.get("provider_settings", {})
207238
runtime = str(provider_settings.get("computer_use_runtime", "local"))
208-
runtime_computer_tools = cls._get_runtime_computer_tools(runtime)
239+
tool_mgr = (
240+
ctx.get_llm_tool_manager()
241+
if hasattr(ctx, "get_llm_tool_manager")
242+
else llm_tools
243+
)
244+
runtime_computer_tools = cls._get_runtime_computer_tools(
245+
runtime,
246+
tool_mgr,
247+
)
209248

210249
# Keep persona semantics aligned with the main agent: tools=None means
211250
# "all tools", including runtime computer-use tools.

0 commit comments

Comments
 (0)