feat: reduce default max_agent_step to 3 and refactor shell execution

- Lower default max_agent_step from 30 to 3 across all agent runners
  (coze, dashscope, deerflow, dify) for faster responses
- Refactor ExecuteShellTool to use plumbum with session-based isolation,
  maintaining shell state per session (cwd, env vars, etc.)
- Remove unused API-specific environment variables from cmd_run
- Fix data access bug in shipyard_neo._maybe_model_dump
- Add plumbum>=1.10.0 dependency
This commit is contained in:
LIghtJUNction
2026-03-21 23:47:43 +08:00
parent 43e107068a
commit f9243a73d5
13 changed files with 128 additions and 42 deletions
+1
View File
@@ -65,3 +65,4 @@ GenieData/
.worktrees/
.env
dashboard/warker.js
+5 -7
View File
@@ -306,13 +306,6 @@ def run(
"ASTRBOT_SSL_CERT",
"ASTRBOT_SSL_KEY",
"ASTRBOT_SSL_CA_CERTS",
# API specific envs
"ASTRBOT_API_HOST",
"ASTRBOT_API_PORT",
"ASTRBOT_API_SSL_ENABLE",
"ASTRBOT_API_SSL_CERT",
"ASTRBOT_API_SSL_KEY",
"ASTRBOT_API_SSL_CA_CERTS",
"http_proxy",
"https_proxy",
"no_proxy",
@@ -341,6 +334,11 @@ def run(
lock_file = astrbot_root / "astrbot.lock"
lock = FileLock(lock_file, timeout=5)
with lock.acquire():
click.echo("AstrBot is running...")
if backend_only:
click.echo("Visit the dashboard at : https://dash.astrbot.men/")
click.echo("Backend Requests : localhost or based on https")
asyncio.run(run_astrbot(astrbot_root))
except KeyboardInterrupt:
click.echo("AstrBot has been shut down.")
@@ -107,7 +107,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]):
@override
async def step_until_done(
self, max_step: int = 30
self, max_step: int = 3
) -> T.AsyncGenerator[AgentResponse, None]:
while not self.done():
async for resp in self.step():
@@ -117,7 +117,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]):
@override
async def step_until_done(
self, max_step: int = 30
self, max_step: int = 3
) -> T.AsyncGenerator[AgentResponse, None]:
while not self.done():
async for resp in self.step():
@@ -304,7 +304,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]):
@override
async def step_until_done(
self, max_step: int = 30
self, max_step: int = 3
) -> T.AsyncGenerator[AgentResponse, None]:
if max_step <= 0:
raise ValueError("max_step must be greater than 0")
@@ -1,10 +1,16 @@
import base64
import os
import sys
import typing as T
from collections.abc import AsyncGenerator
from typing import Any
import astrbot.core.message.components as Comp
from astrbot.core import logger, sp
from astrbot.core.agent.hooks import BaseAgentRunHooks
from astrbot.core.agent.response import AgentResponseData
from astrbot.core.agent.run_context import ContextWrapper, TContext
from astrbot.core.agent.runners.base import AgentResponse, AgentState, BaseAgentRunner
from astrbot.core.agent.runners.dify.dify_api_client import DifyAPIClient
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.provider.entities import (
LLMResponse,
@@ -13,12 +19,6 @@ from astrbot.core.provider.entities import (
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
from astrbot.core.utils.io import download_file
from ...hooks import BaseAgentRunHooks
from ...response import AgentResponseData
from ...run_context import ContextWrapper, TContext
from ..base import AgentResponse, AgentState, BaseAgentRunner
from .dify_api_client import DifyAPIClient
if sys.version_info >= (3, 12):
from typing import override
else:
@@ -35,7 +35,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
run_context: ContextWrapper[TContext],
agent_hooks: BaseAgentRunHooks[TContext],
provider_config: dict,
**kwargs: T.Any,
**kwargs: Any,
) -> None:
self.req = request
self.streaming = kwargs.get("streaming", False)
@@ -100,8 +100,8 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
@override
async def step_until_done(
self, max_step: int = 30
) -> T.AsyncGenerator[AgentResponse, None]:
self, max_step: int = 3
) -> AsyncGenerator[AgentResponse, None]:
while not self.done():
async for resp in self.step():
yield resp
+2 -2
View File
@@ -106,7 +106,7 @@ def _extract_final_streaming_chain(msg_chain: MessageChain) -> MessageChain | No
async def run_agent(
agent_runner: AgentRunner,
max_step: int = 30,
max_step: int = 3,
show_tool_use: bool = True,
show_tool_call_result: bool = False,
stream_to_general: bool = False,
@@ -301,7 +301,7 @@ async def _watch_agent_stop_signal(agent_runner: AgentRunner, astr_event) -> Non
async def run_live_agent(
agent_runner: AgentRunner,
tts_provider: TTSProvider | None = None,
max_step: int = 30,
max_step: int = 3,
show_tool_use: bool = True,
show_tool_call_result: bool = False,
show_reasoning: bool = False,
+2 -2
View File
@@ -357,7 +357,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
continue
prov_settings: dict = ctx.get_config(umo=umo).get("provider_settings", {})
agent_max_step = int(prov_settings.get("max_agent_step", 30))
agent_max_step = int(prov_settings.get("max_agent_step", 3))
stream = prov_settings.get("streaming_response", False)
llm_resp = await ctx.tool_loop_agent(
event=event,
@@ -583,7 +583,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
return
runner = result.agent_runner
async for _ in runner.step_until_done(30):
async for _ in runner.step_until_done(3):
# agent will send message to user via using tools
pass
llm_resp = runner.get_final_llm_resp()
@@ -12,13 +12,13 @@ from astrbot.api import logger
if TYPE_CHECKING:
from astrbot.core.agent.tool import FunctionTool
from ..olayer import (
from astrbot.core.computer.booters.base import ComputerBooter
from astrbot.core.computer.olayer import (
BrowserComponent,
FileSystemComponent,
PythonComponent,
ShellComponent,
)
from .base import ComputerBooter
def _maybe_model_dump(value: Any) -> dict[str, Any]:
@@ -49,7 +49,7 @@ class NeoPythonComponent(PythonComponent):
output_text = payload.get("output", "") or ""
error_text = payload.get("error", "") or ""
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
rich_output = data.get("output") if isinstance(data.get("output"), dict) else {}
rich_output = (data.get("output") or {}) if isinstance(data, dict) else {}
if not isinstance(rich_output.get("images"), list):
rich_output["images"] = []
if "text" not in rich_output:
+99 -14
View File
@@ -1,17 +1,28 @@
from __future__ import annotations
import json
from dataclasses import dataclass, field
from typing import Any
from plumbum import local
from plumbum.commands.processes import ProcessExecutionError
from astrbot.api import FunctionTool
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import ToolExecResult
from astrbot.core.astr_agent_context import AstrAgentContext
from ..computer_client import get_booter, get_local_booter
from .permissions import check_admin_permission
@dataclass
class ExecuteShellTool(FunctionTool):
"""Stateful shell execution tool based on plumbum.
Each session maintains its own shell instance, ensuring state isolation
across different sessions (such as working directory, environment variables, etc.).
"""
name: str = "astrbot_execute_shell"
description: str = "Execute a command in the shell."
parameters: dict = field(
@@ -39,6 +50,24 @@ class ExecuteShellTool(FunctionTool):
)
is_local: bool = False
_session_shells: dict[str, Any] = field(
default_factory=dict, init=False, repr=False
)
def _get_session_shell(self, session_id: str) -> Any:
"""Get or create a shell instance for a specific session.
Args:
session_id: The unique session identifier.
Returns:
plumbum shell instance for this session.
"""
import os
if session_id not in self._session_shells:
self._session_shells[session_id] = local.cwd(os.getcwd())
return self._session_shells[session_id]
async def call(
self,
@@ -50,26 +79,82 @@ class ExecuteShellTool(FunctionTool):
if permission_error := check_admin_permission(context, "Shell execution"):
return permission_error
if self.is_local:
sb = get_local_booter()
else:
sb = await get_booter(
context.context.context,
context.context.event.unified_msg_origin,
)
try:
config = context.context.context.get_config(
umo=context.context.event.unified_msg_origin
)
# Get session ID for per-session shell isolation
session_id = context.context.event.unified_msg_origin
shell = self._get_session_shell(session_id)
config = context.context.context.get_config(umo=session_id)
try:
timeout = int(
config.get("provider_settings", {}).get("tool_call_timeout", 30)
)
except (ValueError, TypeError):
timeout = 30
result = await sb.shell.exec(
command, background=background, env=env, timeout=timeout
)
# Merge environment variables
if env:
full_env = shell.env.copy()
full_env.update(env)
else:
full_env = None
if background:
# Background execution: use & to run in background, no waiting
cmd_line = f"{command} &"
if full_env:
_, stdout, stderr = shell.run(
["/bin/sh", "-c", cmd_line],
env=full_env,
timeout=timeout,
)
else:
_, stdout, stderr = shell.run(
["/bin/sh", "-c", cmd_line],
timeout=timeout,
)
result = {
"success": True,
"background": True,
"stdout": stdout,
"stderr": stderr,
}
else:
# Foreground execution: wait for command completion
try:
if full_env:
exit_code, stdout, stderr = shell.run(
["/bin/sh", "-c", command],
env=full_env,
timeout=timeout,
)
else:
exit_code, stdout, stderr = shell.run(
["/bin/sh", "-c", command],
timeout=timeout,
)
result = {
"success": exit_code == 0,
"exit_code": exit_code,
"stdout": stdout,
"stderr": stderr,
}
except ProcessExecutionError as e:
result = {
"success": False,
"exit_code": e.exit_code,
"stdout": e.stdout,
"stderr": e.stderr,
}
except Exception as e:
result = {
"success": False,
"exit_code": -1,
"stdout": "",
"stderr": str(e),
}
return json.dumps(result)
except Exception as e:
return f"Error executing command: {e!s}"
+1
View File
@@ -3,3 +3,4 @@ node_modules/
dist/
bun.lock
pnpm-lock.yaml
worker.js
+1 -1
View File
@@ -73,7 +73,7 @@ AstrBot 默认配置如下:
"streaming_response": False,
"show_tool_use_status": False,
"streaming_segmented": False,
"max_agent_step": 30,
"max_agent_step": 3,
"tool_call_timeout": 120,
},
"provider_stt_settings": {
+1
View File
@@ -67,6 +67,7 @@ dependencies = [
"quart-cors>=0.8.0",
"anyio>=4.12.1",
"argon2-cffi>=23.1.0",
"plumbum>=1.10.0",
]
[dependency-groups]