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
38 changes: 19 additions & 19 deletions astrbot/core/astr_agent_tool_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,9 +520,10 @@ async def _wake_main_agent_for_background_result(
extra_result_fields: dict[str, T.Any] | None = None,
) -> None:
from astrbot.core.astr_main_agent import (
MainAgentBuildConfig,
_get_session_conv,
append_proactive_history,
build_main_agent,
build_proactive_agent_config,
)

event = run_context.context.event
Expand All @@ -549,24 +550,17 @@ async def _wake_main_agent_for_background_result(
cron_event.role = event.role
cfg = ctx.get_config(umo=event.unified_msg_origin) or {}
provider_settings = cfg.get("provider_settings") or {}
config = MainAgentBuildConfig(
tool_call_timeout=run_context.tool_call_timeout,
streaming_response=provider_settings.get("stream", False),
config = build_proactive_agent_config(
plugin_context=ctx,
app_config=cfg,
provider_settings=provider_settings,
tool_call_timeout=run_context.tool_call_timeout,
)

req = ProviderRequest()
conv = await _get_session_conv(event=cron_event, plugin_context=ctx)
req.conversation = conv
context = json.loads(conv.history)
if context:
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}"
)
append_proactive_history(req, conv, config)

bg = json.dumps(extras["background_task_result"], ensure_ascii=False)
req.system_prompt += BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT.format(
Expand All @@ -590,14 +584,23 @@ async def _wake_main_agent_for_background_result(
event=cron_event, plugin_context=ctx, config=config, req=req
)
if not result:
logger.error(f"Failed to build main agent for background task {tool_name}.")
return
raise RuntimeError(
f"Failed to build main agent for background task {tool_name}"
)

runner = result.agent_runner
async for _ in runner.step_until_done(30):
async for _ in runner.step_until_done(config.max_agent_step):
# agent will send message to user via using tools
pass
llm_resp = runner.get_final_llm_resp()
if not llm_resp or llm_resp.role == "err":
error_text = (
llm_resp.completion_text
if llm_resp and llm_resp.completion_text
else "Background task agent returned no usable response"
)
raise RuntimeError(error_text)

task_meta = extras.get("background_task_result", {})
summary_note = (
f"[BackgroundTask] {summary_name} "
Expand All @@ -614,9 +617,6 @@ async def _wake_main_agent_for_background_result(
req=req,
summary_note=summary_note,
)
if not llm_resp:
logger.warning("background task agent got no response")
return

@classmethod
async def _execute_local(
Expand Down
159 changes: 158 additions & 1 deletion astrbot/core/astr_main_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
from pathlib import Path

from astrbot.core import logger
from astrbot.core.agent.context.token_counter import EstimateTokenCounter
from astrbot.core.agent.context.truncator import ContextTruncator
from astrbot.core.agent.handoff import HandoffTool
from astrbot.core.agent.mcp_client import MCPTool
from astrbot.core.agent.message import TextPart
from astrbot.core.agent.message import Message, TextPart
from astrbot.core.agent.tool import ToolSet
from astrbot.core.astr_agent_context import AgentContextWrapper, AstrAgentContext
from astrbot.core.astr_agent_hooks import MAIN_AGENT_HOOKS
Expand Down Expand Up @@ -212,6 +214,8 @@ class MainAgentBuildConfig:
timezone: str | None = None
max_quoted_fallback_images: int = 20
"""Maximum number of images injected from quoted-message fallback extraction."""
max_agent_step: int = 30
"""Maximum number of agent steps for callers that drive the runner directly."""


@dataclass(slots=True)
Expand All @@ -222,6 +226,159 @@ class MainAgentBuildResult:
reset_coro: Coroutine | None = None


PROACTIVE_HISTORY_MAX_TOKENS = 8192


def build_proactive_agent_config(
*,
plugin_context: Context,
app_config: dict,
provider_settings: dict,
tool_call_timeout: int | None = None,
streaming_response: bool | None = None,
llm_safety_mode: bool | None = None,
add_cron_tools: bool | None = None,
) -> MainAgentBuildConfig:
"""Build a main-agent config for proactive entry points.

Proactive jobs must use the same provider settings as normal messages. The
explicit overrides are limited to behavior that is inherent to a wake-up
path, such as disabling streaming for cron delivery.

Args:
plugin_context: AstrBot context used for global configuration lookup.
app_config: Global AstrBot configuration.
provider_settings: Provider and agent settings.
tool_call_timeout: Optional timeout override for tool calls.
streaming_response: Optional streaming override for the caller.
llm_safety_mode: Optional safety-mode override for the caller.
add_cron_tools: Optional cron-tool availability override for the caller.

Returns:
Configuration shared by proactive and normal agent entry points.
"""
settings = provider_settings or {}
file_extract_conf = settings.get("file_extract", {}) or {}
global_config = app_config or {}
proactive_cfg = settings.get("proactive_capability", {}) or {}
if streaming_response is None:
streaming_response = settings.get(
"streaming_response", settings.get("stream", False)
)
if add_cron_tools is None:
add_cron_tools = proactive_cfg.get("add_cron_tools", True)

return MainAgentBuildConfig(
tool_call_timeout=int(
settings.get("tool_call_timeout", 120)
if tool_call_timeout is None
else tool_call_timeout
),
tool_schema_mode=settings.get("tool_schema_mode", "full"),
streaming_response=bool(streaming_response),
sanitize_context_by_modalities=bool(
settings.get("sanitize_context_by_modalities", False)
),
kb_agentic_mode=bool(global_config.get("kb_agentic_mode", False)),
file_extract_enabled=bool(file_extract_conf.get("enable", False)),
file_extract_prov=file_extract_conf.get("provider", "moonshotai"),
file_extract_msh_api_key=file_extract_conf.get("moonshotai_api_key", ""),
context_limit_reached_strategy=settings.get(
"context_limit_reached_strategy", "truncate_by_turns"
),
llm_compress_instruction=settings.get("llm_compress_instruction", ""),
llm_compress_keep_recent_ratio=float(
settings.get("llm_compress_keep_recent_ratio", 0.15)
),
llm_compress_provider_id=settings.get("llm_compress_provider_id", ""),
max_context_length=int(settings.get("max_context_length", -1)),
dequeue_context_length=int(settings.get("dequeue_context_length", 1)),
fallback_max_context_tokens=int(
settings.get("fallback_max_context_tokens", 128000)
),
llm_safety_mode=bool(
settings.get("llm_safety_mode", True)
if llm_safety_mode is None
else llm_safety_mode
),
safety_mode_strategy=settings.get("safety_mode_strategy", "system_prompt"),
computer_use_runtime=settings.get("computer_use_runtime", "none"),
sandbox_cfg=settings.get("sandbox", {}) or {},
add_cron_tools=bool(add_cron_tools),
provider_settings=settings,
subagent_orchestrator=global_config.get("subagent_orchestrator", {}) or {},
timezone=global_config.get("timezone")
or plugin_context.get_config().get("timezone"),
max_quoted_fallback_images=int(settings.get("max_quoted_fallback_images", 20)),
max_agent_step=int(settings.get("max_agent_step", 30)),
)


def append_proactive_history(
req: ProviderRequest,
conversation: Conversation,
config: MainAgentBuildConfig,
) -> None:
"""Add bounded conversation history to a proactive system prompt.

Wake-up prompts intentionally describe history as reference material, but
they still need a hard bound. Otherwise they bypass the runner's normal
context manager because proactive callers flatten history into the system
prompt.

Args:
req: Provider request receiving the formatted history.
conversation: Conversation whose history should be included.
config: Agent configuration controlling history truncation.
"""
try:
raw_context = json.loads(conversation.history or "[]")
except Exception as exc: # noqa: BLE001
logger.warning("Failed to parse proactive conversation history: %s", exc)
return

if not raw_context:
return

messages: list[Message] = []
for item in raw_context:
try:
messages.append(Message.model_validate(item))
except Exception: # noqa: BLE001
logger.debug("Skip malformed proactive history item: %r", item)

if not messages:
return

truncator = ContextTruncator()
if config.max_context_length != -1:
messages = truncator.truncate_by_turns(
messages,
keep_most_recent_turns=config.max_context_length,
drop_turns=max(config.dequeue_context_length, 1),
)

token_counter = EstimateTokenCounter()
while (
len(messages) > 2
and token_counter.count_tokens(messages) > PROACTIVE_HISTORY_MAX_TOKENS
):
next_messages = truncator.truncate_by_dropping_oldest_turns(
messages, drop_turns=max(config.dequeue_context_length, 1)
)
if len(next_messages) >= len(messages):
break
messages = next_messages

req.contexts = [message.model_dump(exclude_none=True) for message in messages]
context_dump = req._print_friendly_context()
req.contexts = []
req.system_prompt += (
"\n\nBelow is bounded previous conversation history for reference only:\n"
f"---\n{context_dump}\n---\n"
)


def _set_llm_error_message(event: AstrMessageEvent, message: str) -> None:
event.set_extra(LLM_ERROR_MESSAGE_EXTRA_KEY, message)

Expand Down
4 changes: 4 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
"reachability_check": False,
"max_agent_step": 30,
"tool_call_timeout": 120,
"cron_job_timeout": 3600,
"tool_schema_mode": "full",
"llm_safety_mode": True,
"safety_mode_strategy": "system_prompt", # TODO: llm judge
Expand Down Expand Up @@ -2931,6 +2932,9 @@
"tool_call_timeout": {
"type": "int",
},
"cron_job_timeout": {
"type": "int",
},
"tool_schema_mode": {
"type": "string",
},
Expand Down
Loading
Loading