From 193676012f43f71f3781f67bbe87a51d95114979 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sun, 1 Feb 2026 17:42:08 +0800 Subject: [PATCH] feat: implement history persistence for agent interactions and enhance cron job permission handling --- astrbot/core/astr_agent_tool_exec.py | 19 +++++++++++++++++ astrbot/core/astr_main_agent.py | 2 +- astrbot/core/cron/manager.py | 32 ++++++++++++++++++++++++++-- astrbot/core/db/po.py | 4 +--- astrbot/core/utils/history_saver.py | 31 +++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 astrbot/core/utils/history_saver.py diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 523f917eb..460cab332 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -28,6 +28,7 @@ from astrbot.core.message.message_event_result import ( 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.utils.history_saver import persist_agent_history class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): @@ -199,6 +200,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): extras=extras, message_type=session.message_type, ) + cron_event.role = event.role config = MainAgentBuildConfig(tool_call_timeout=3600) req = ProviderRequest() @@ -221,6 +223,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): req.prompt = ( "Proceed according to your system instructions. " "Output using same language as previous conversation." + " After completing your task, summarize and output your actions and results." ) if not req.func_tool: req.func_tool = ToolSet() @@ -238,6 +241,22 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): # agent will send message to user via using tools pass llm_resp = runner.get_final_llm_resp() + task_meta = extras.get("background_task_result", {}) + summary_note = ( + f"[BackgroundTask] {task_meta.get('tool_name', tool.name)} " + f"(task_id={task_meta.get('task_id', task_id)}) finished. " + f"Result: {task_meta.get('result') or result_text or 'no content'}" + ) + if llm_resp and llm_resp.completion_text: + summary_note += ( + f"I finished the task, here is the result: {llm_resp.completion_text}" + ) + await persist_agent_history( + ctx.conversation_manager, + event=cron_event, + req=req, + summary_note=summary_note, + ) if not llm_resp: logger.warning("background task agent got no response") return diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 86596d43c..41aa0fe42 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -31,9 +31,9 @@ from astrbot.core.astr_main_agent_resources import ( LOCAL_PYTHON_TOOL, PYTHON_TOOL, SANDBOX_MODE_PROMPT, + SEND_MESSAGE_TO_USER_TOOL, TOOL_CALL_PROMPT, TOOL_CALL_PROMPT_SKILLS_LIKE_MODE, - SEND_MESSAGE_TO_USER_TOOL, retrieve_knowledge_base, ) from astrbot.core.conversation_mgr import Conversation diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index 06e88649e..9401e551d 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -15,6 +15,7 @@ from astrbot.core.db import BaseDatabase from astrbot.core.db.po import CronJob from astrbot.core.platform.message_session import MessageSession from astrbot.core.provider.entites import ProviderRequest +from astrbot.core.utils.history_saver import persist_agent_history if TYPE_CHECKING: from astrbot.core.star.context import Context @@ -182,7 +183,7 @@ class CronJobManager: if job.job_type == "basic": await self._run_basic_job(job) elif job.job_type == "active_agent": - await self._run_active_agent_job(job) + await self._run_active_agent_job(job, start_time=start_time) else: raise ValueError(f"Unknown cron job type: {job.job_type}") except Exception as e: # noqa: BLE001 @@ -208,7 +209,7 @@ class CronJobManager: if asyncio.iscoroutine(result): await result - async def _run_active_agent_job(self, job: CronJob): + async def _run_active_agent_job(self, job: CronJob, start_time: datetime): payload = job.payload or {} session_str = payload.get("session") if not session_str: @@ -222,6 +223,7 @@ class CronJobManager: "type": job.job_type, "description": job.description, "note": note, + "run_started_at": start_time.isoformat(), }, "cron_payload": payload, } @@ -268,6 +270,15 @@ class CronJobManager: message_type=session.message_type, ) + # judge user's role + umo = cron_event.unified_msg_origin + cfg = self.ctx.get_config(umo=umo) + admin_ids = cfg.get("admins_id", []) + if admin_ids: + cron_event.role = ( + "admin" if cron_event.get_sender_id() in admin_ids else "member" + ) + config = MainAgentBuildConfig( tool_call_timeout=3600, llm_safety_mode=False, @@ -295,6 +306,7 @@ class CronJobManager: "You are now responding to a scheduled task" "Proceed according to your system instructions. " "Output using same language as previous conversation." + "After completing your task, summarize and output your actions and results." ) if not req.func_tool: req.func_tool = ToolSet() @@ -312,6 +324,22 @@ class CronJobManager: # agent will send message to user via using tools pass llm_resp = runner.get_final_llm_resp() + cron_meta = extras.get("cron_job", {}) if extras else {} + summary_note = ( + f"[CronJob] {cron_meta.get('name') or cron_meta.get('id', 'unknown')}: {cron_meta.get('description', '')} " + f" triggered at {cron_meta.get('run_started_at', 'unknown time')}, " + ) + if llm_resp and llm_resp.role == "assistant": + summary_note += ( + f"I finished this job, here is the result: {llm_resp.completion_text}" + ) + + await persist_agent_history( + self.ctx.conversation_manager, + event=cron_event, + req=req, + summary_note=summary_note, + ) if not llm_resp: logger.warning("Cron job agent got no response") return diff --git a/astrbot/core/db/po.py b/astrbot/core/db/po.py index d676fe845..a63e5ea1e 100644 --- a/astrbot/core/db/po.py +++ b/astrbot/core/db/po.py @@ -157,9 +157,7 @@ class CronJob(TimestampMixin, SQLModel, table=True): ) name: str = Field(max_length=255, nullable=False) description: str | None = Field(default=None, sa_type=Text) - job_type: str = Field( - max_length=32, nullable=False - ) # basic | active_agent + job_type: str = Field(max_length=32, nullable=False) # basic | active_agent cron_expression: str | None = Field(default=None, max_length=255) timezone: str | None = Field(default=None, max_length=64) payload: dict = Field(default_factory=dict, sa_type=JSON) diff --git a/astrbot/core/utils/history_saver.py b/astrbot/core/utils/history_saver.py new file mode 100644 index 000000000..840d3f187 --- /dev/null +++ b/astrbot/core/utils/history_saver.py @@ -0,0 +1,31 @@ +import json + +from astrbot import logger +from astrbot.core.conversation_mgr import ConversationManager +from astrbot.core.platform.astr_message_event import AstrMessageEvent +from astrbot.core.provider.entities import ProviderRequest + + +async def persist_agent_history( + conversation_manager: ConversationManager, + *, + event: AstrMessageEvent, + req: ProviderRequest, + summary_note: str, +) -> None: + """Persist agent interaction into conversation history.""" + if not req or not req.conversation: + return + + history = [] + try: + history = json.loads(req.conversation.history or "[]") + except Exception as exc: # noqa: BLE001 + logger.warning("Failed to parse conversation history: %s", exc) + history.append({"role": "user", "content": "Output your last task result below."}) + history.append({"role": "assistant", "content": summary_note}) + await conversation_manager.update_conversation( + event.unified_msg_origin, + req.conversation.cid, + history=history, + )