AstrBot 4.13.2__py3-none-any.whl → 4.14.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. astrbot/builtin_stars/astrbot/main.py +0 -6
  2. astrbot/builtin_stars/session_controller/main.py +1 -2
  3. astrbot/cli/__init__.py +1 -1
  4. astrbot/core/agent/agent.py +2 -1
  5. astrbot/core/agent/handoff.py +14 -1
  6. astrbot/core/agent/runners/tool_loop_agent_runner.py +14 -1
  7. astrbot/core/agent/tool.py +5 -0
  8. astrbot/core/astr_agent_run_util.py +21 -3
  9. astrbot/core/astr_agent_tool_exec.py +178 -3
  10. astrbot/core/astr_main_agent.py +980 -0
  11. astrbot/core/astr_main_agent_resources.py +453 -0
  12. astrbot/core/computer/computer_client.py +10 -1
  13. astrbot/core/computer/tools/fs.py +22 -14
  14. astrbot/core/config/default.py +84 -58
  15. astrbot/core/core_lifecycle.py +43 -1
  16. astrbot/core/cron/__init__.py +3 -0
  17. astrbot/core/cron/events.py +67 -0
  18. astrbot/core/cron/manager.py +376 -0
  19. astrbot/core/db/__init__.py +60 -0
  20. astrbot/core/db/po.py +31 -0
  21. astrbot/core/db/sqlite.py +120 -0
  22. astrbot/core/event_bus.py +0 -1
  23. astrbot/core/message/message_event_result.py +21 -3
  24. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +111 -580
  25. astrbot/core/pipeline/scheduler.py +0 -2
  26. astrbot/core/platform/astr_message_event.py +5 -5
  27. astrbot/core/platform/platform.py +9 -0
  28. astrbot/core/platform/platform_metadata.py +2 -0
  29. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +1 -0
  30. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +1 -0
  31. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +1 -0
  32. astrbot/core/platform/sources/webchat/webchat_adapter.py +1 -0
  33. astrbot/core/platform/sources/wecom/wecom_adapter.py +1 -0
  34. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +1 -0
  35. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +1 -0
  36. astrbot/core/provider/entities.py +1 -1
  37. astrbot/core/skills/skill_manager.py +9 -8
  38. astrbot/core/star/context.py +8 -0
  39. astrbot/core/star/filter/custom_filter.py +3 -3
  40. astrbot/core/star/register/star_handler.py +1 -1
  41. astrbot/core/subagent_orchestrator.py +96 -0
  42. astrbot/core/tools/cron_tools.py +174 -0
  43. astrbot/core/utils/history_saver.py +31 -0
  44. astrbot/core/utils/trace.py +4 -0
  45. astrbot/dashboard/routes/__init__.py +4 -0
  46. astrbot/dashboard/routes/cron.py +174 -0
  47. astrbot/dashboard/routes/log.py +36 -0
  48. astrbot/dashboard/routes/plugin.py +11 -0
  49. astrbot/dashboard/routes/skills.py +12 -37
  50. astrbot/dashboard/routes/subagent.py +117 -0
  51. astrbot/dashboard/routes/tools.py +41 -14
  52. astrbot/dashboard/server.py +3 -0
  53. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/METADATA +21 -2
  54. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/RECORD +57 -51
  55. astrbot/builtin_stars/astrbot/process_llm_request.py +0 -308
  56. astrbot/builtin_stars/reminder/main.py +0 -266
  57. astrbot/builtin_stars/reminder/metadata.yaml +0 -4
  58. astrbot/core/pipeline/process_stage/utils.py +0 -219
  59. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/WHEEL +0 -0
  60. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/entry_points.txt +0 -0
  61. {astrbot-4.13.2.dist-info → astrbot-4.14.1.dist-info}/licenses/LICENSE +0 -0
@@ -85,6 +85,4 @@ class PipelineScheduler:
85
85
  if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent):
86
86
  await event.send(None)
87
87
 
88
- event.trace.record("event_end")
89
-
90
88
  logger.debug("pipeline 执行完毕。")
@@ -22,6 +22,7 @@ from astrbot.core.message.components import (
22
22
  from astrbot.core.message.message_event_result import MessageChain, MessageEventResult
23
23
  from astrbot.core.platform.message_type import MessageType
24
24
  from astrbot.core.provider.entities import ProviderRequest
25
+ from astrbot.core.agent.tool import ToolSet
25
26
  from astrbot.core.utils.metrics import Metric
26
27
  from astrbot.core.utils.trace import TraceSpan
27
28
 
@@ -73,9 +74,6 @@ class AstrMessageEvent(abc.ABC):
73
74
  self.span = self.trace
74
75
  """事件级 TraceSpan(别名: span)"""
75
76
 
76
- self.trace.record("umo", umo=self.unified_msg_origin)
77
- self.trace.record("event_created", created_at=self.created_at)
78
-
79
77
  self._has_send_oper = False
80
78
  """在此次事件中是否有过至少一次发送消息的操作"""
81
79
  self.call_llm = False
@@ -358,6 +356,7 @@ class AstrMessageEvent(abc.ABC):
358
356
  self,
359
357
  prompt: str,
360
358
  func_tool_manager=None,
359
+ tool_set: ToolSet | None = None,
361
360
  session_id: str = "",
362
361
  image_urls: list[str] | None = None,
363
362
  contexts: list | None = None,
@@ -380,7 +379,7 @@ class AstrMessageEvent(abc.ABC):
380
379
 
381
380
  contexts: 当指定 contexts 时,将会使用 contexts 作为上下文。如果同时传入了 conversation,将会忽略 conversation。
382
381
 
383
- func_tool_manager: 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。
382
+ func_tool_manager: [Deprecated] 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。已过时,请使用 tool_set 参数代替。
384
383
 
385
384
  conversation: 可选。如果指定,将在指定的对话中进行 LLM 请求。对话的人格会被用于 LLM 请求,并且结果将会被记录到对话中。
386
385
 
@@ -396,7 +395,8 @@ class AstrMessageEvent(abc.ABC):
396
395
  prompt=prompt,
397
396
  session_id=session_id,
398
397
  image_urls=image_urls,
399
- func_tool=func_tool_manager,
398
+ # func_tool=func_tool_manager,
399
+ func_tool=tool_set,
400
400
  contexts=contexts,
401
401
  system_prompt=system_prompt,
402
402
  conversation=conversation,
@@ -90,6 +90,14 @@ class Platform(abc.ABC):
90
90
  def get_stats(self) -> dict:
91
91
  """获取平台统计信息"""
92
92
  meta = self.meta()
93
+ meta_info = {
94
+ "id": meta.id,
95
+ "name": meta.name,
96
+ "display_name": meta.adapter_display_name or meta.name,
97
+ "description": meta.description,
98
+ "support_streaming_message": meta.support_streaming_message,
99
+ "support_proactive_message": meta.support_proactive_message,
100
+ }
93
101
  return {
94
102
  "id": meta.id or self.config.get("id"),
95
103
  "type": meta.name,
@@ -105,6 +113,7 @@ class Platform(abc.ABC):
105
113
  if self.last_error
106
114
  else None,
107
115
  "unified_webhook": self.unified_webhook(),
116
+ "meta": meta_info,
108
117
  }
109
118
 
110
119
  @abc.abstractmethod
@@ -19,3 +19,5 @@ class PlatformMetadata:
19
19
 
20
20
  support_streaming_message: bool = True
21
21
  """平台是否支持真实流式传输"""
22
+ support_proactive_message: bool = True
23
+ """平台是否支持主动消息推送(非用户触发)"""
@@ -99,6 +99,7 @@ class DingtalkPlatformAdapter(Platform):
99
99
  description="钉钉机器人官方 API 适配器",
100
100
  id=cast(str, self.config.get("id")),
101
101
  support_streaming_message=True,
102
+ support_proactive_message=False,
102
103
  )
103
104
 
104
105
  async def create_message_card(
@@ -136,6 +136,7 @@ class QQOfficialPlatformAdapter(Platform):
136
136
  name="qq_official",
137
137
  description="QQ 机器人官方 API 适配器",
138
138
  id=cast(str, self.config.get("id")),
139
+ support_proactive_message=False,
139
140
  )
140
141
 
141
142
  @staticmethod
@@ -118,6 +118,7 @@ class QQOfficialWebhookPlatformAdapter(Platform):
118
118
  name="qq_official_webhook",
119
119
  description="QQ 机器人官方 API 适配器",
120
120
  id=cast(str, self.config.get("id")),
121
+ support_proactive_message=False,
121
122
  )
122
123
 
123
124
  async def run(self):
@@ -86,6 +86,7 @@ class WebChatAdapter(Platform):
86
86
  name="webchat",
87
87
  description="webchat",
88
88
  id="webchat",
89
+ support_proactive_message=False,
89
90
  )
90
91
 
91
92
  async def send_by_session(
@@ -224,6 +224,7 @@ class WecomPlatformAdapter(Platform):
224
224
  "wecom 适配器",
225
225
  id=self.config.get("id", "wecom"),
226
226
  support_streaming_message=False,
227
+ support_proactive_message=False,
227
228
  )
228
229
 
229
230
  @override
@@ -128,6 +128,7 @@ class WecomAIBotAdapter(Platform):
128
128
  name="wecom_ai_bot",
129
129
  description="企业微信智能机器人适配器,支持 HTTP 回调接收消息",
130
130
  id=self.config.get("id", "wecom_ai_bot"),
131
+ support_proactive_message=False,
131
132
  )
132
133
 
133
134
  # 初始化 API 客户端
@@ -228,6 +228,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
228
228
  "微信公众平台 适配器",
229
229
  id=self.config.get("id", "weixin_official_account"),
230
230
  support_streaming_message=False,
231
+ support_proactive_message=False,
231
232
  )
232
233
 
233
234
  @override
@@ -165,7 +165,7 @@ class ProviderRequest:
165
165
 
166
166
  result_parts.append(f"{role}: {''.join(msg_parts)}")
167
167
 
168
- return result_parts
168
+ return "\n".join(result_parts)
169
169
 
170
170
  async def assemble_context(self) -> dict:
171
171
  """将请求(prompt 和 image_urls)包装成 OpenAI 的消息格式。"""
@@ -62,6 +62,7 @@ def build_skills_prompt(skills: list[SkillInfo]) -> str:
62
62
  # Based on openai/codex
63
63
  return (
64
64
  "## Skills\n"
65
+ "You have many useful skills that can help you accomplish various tasks.\n"
65
66
  "A skill is a set of local instructions stored in a `SKILL.md` file.\n"
66
67
  "### Available skills\n"
67
68
  f"{skills_block}\n"
@@ -69,21 +70,21 @@ def build_skills_prompt(skills: list[SkillInfo]) -> str:
69
70
  "\n"
70
71
  "- Discovery: The list above shows all skills available in this session. Full instructions live in the referenced `SKILL.md`.\n"
71
72
  "- Trigger rules: Use a skill if the user names it or the task matches its description. Do not carry skills across turns unless re-mentioned\n"
72
- "- Unavailable: If a skill is missing or unreadable, say so and fallback.\n"
73
73
  "### How to use a skill (progressive disclosure):\n"
74
- " 1) After deciding to use a skill, open its `SKILL.md` and read only what is necessary to follow the workflow.\n"
75
- " 2) Load only directly referenced files, DO NOT bulk-load everything.\n"
76
- " 3) If `scripts/` exist, prefer running or patching them instead of retyping large blocks of code.\n"
77
- " 4) If `assets/` or templates exist, reuse them rather than recreating everything from scratch.\n"
74
+ " 0) Mandatory grounding: Before using any skill, you MUST inspect its `SKILL.md` using shell tools"
75
+ " (e.g., `cat`, `head`, `sed`, `awk`, `grep`). Do not rely on assumptions or memory.\n"
76
+ " 1) Load only directly referenced files, DO NOT bulk-load everything.\n"
77
+ " 2) If `scripts/` exist, prefer running or patching them instead of retyping large blocks of code.\n"
78
+ " 3) If `assets/` or templates exist, reuse them rather than recreating everything from scratch.\n"
78
79
  "- Coordination:\n"
79
80
  " - If multiple skills apply, choose the minimal set that covers the request and state the order in which you will use them.\n"
80
81
  " - Announce which skill(s) you are using and why (one short line). If you skip an obvious skill, explain why.\n"
81
82
  " - Prefer to use `astrbot_*` tools to perform skills that need to run scripts.\n"
82
83
  "- Context hygiene:\n"
83
- " - Keep context small: summarize long sections instead of pasting them, and load extra files only when necessary.\n"
84
84
  " - Avoid deep reference chasing: unless blocked, open only files that are directly linked from `SKILL.md`.\n"
85
- " - When variants exist (frameworks, providers, domains), select only the relevant reference file(s) and note that choice.\n"
86
- "- Failure handling: If a skill cannot be applied, state the issue and continue with the best alternative."
85
+ "- Failure handling: If a skill cannot be applied, state the issue and continue with the best alternative.\n"
86
+ "### Example\n"
87
+ "When you decided to use a skill, use shell tool to read its `SKILL.md`, e.g., `head -40 skills/code_formatter/SKILL.md`, and you can increase or decrease the number of lines as needed.\n"
87
88
  )
88
89
 
89
90
 
@@ -12,6 +12,7 @@ from astrbot.core.agent.tool import ToolSet
12
12
  from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
13
13
  from astrbot.core.config.astrbot_config import AstrBotConfig
14
14
  from astrbot.core.conversation_mgr import ConversationManager
15
+ from astrbot.core.cron.manager import CronJobManager
15
16
  from astrbot.core.db import BaseDatabase
16
17
  from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
17
18
  from astrbot.core.message.message_event_result import MessageChain
@@ -34,6 +35,7 @@ from astrbot.core.star.filter.platform_adapter_type import (
34
35
  ADAPTER_NAME_2_TYPE,
35
36
  PlatformAdapterType,
36
37
  )
38
+ from astrbot.core.subagent_orchestrator import SubAgentOrchestrator
37
39
 
38
40
  from ..exceptions import ProviderNotFoundError
39
41
  from .filter.command import CommandFilter
@@ -65,6 +67,8 @@ class Context:
65
67
  persona_manager: PersonaManager,
66
68
  astrbot_config_mgr: AstrBotConfigManager,
67
69
  knowledge_base_manager: KnowledgeBaseManager,
70
+ cron_manager: CronJobManager,
71
+ subagent_orchestrator: SubAgentOrchestrator | None = None,
68
72
  ):
69
73
  self._event_queue = event_queue
70
74
  """事件队列。消息平台通过事件队列传递消息事件。"""
@@ -86,6 +90,9 @@ class Context:
86
90
  """配置文件管理器(非webui)"""
87
91
  self.kb_manager = knowledge_base_manager
88
92
  """知识库管理器"""
93
+ self.cron_manager = cron_manager
94
+ """Cron job manager, initialized by core lifecycle."""
95
+ self.subagent_orchestrator = subagent_orchestrator
89
96
 
90
97
  async def llm_generate(
91
98
  self,
@@ -463,6 +470,7 @@ class Context:
463
470
  _parts.append(part)
464
471
  if part in flags and i + 1 < len(module_part):
465
472
  _parts.append(module_part[i + 1])
473
+ module_part.append("main")
466
474
  break
467
475
  tool.handler_module_path = ".".join(_parts)
468
476
  module_path = tool.handler_module_path
@@ -37,9 +37,9 @@ class CustomFilter(HandlerFilter, metaclass=CustomFilterMeta):
37
37
  class CustomFilterOr(CustomFilter):
38
38
  def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
39
39
  super().__init__()
40
- if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr):
40
+ if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
41
41
  raise ValueError(
42
- "CustomFilter lass can only operate with other CustomFilter.",
42
+ "CustomFilter class can only operate with other CustomFilter.",
43
43
  )
44
44
  self.filter1 = filter1
45
45
  self.filter2 = filter2
@@ -51,7 +51,7 @@ class CustomFilterOr(CustomFilter):
51
51
  class CustomFilterAnd(CustomFilter):
52
52
  def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
53
53
  super().__init__()
54
- if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr):
54
+ if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
55
55
  raise ValueError(
56
56
  "CustomFilter lass can only operate with other CustomFilter.",
57
57
  )
@@ -150,7 +150,7 @@ def register_custom_filter(custom_type_filter, *args, **kwargs):
150
150
  if args:
151
151
  raise_error = args[0]
152
152
 
153
- if not isinstance(custom_filter, CustomFilterAnd | CustomFilterOr):
153
+ if not isinstance(custom_filter, (CustomFilterAnd, CustomFilterOr)):
154
154
  custom_filter = custom_filter(raise_error)
155
155
 
156
156
  def decorator(awaitable):
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from astrbot import logger
6
+ from astrbot.core.agent.agent import Agent
7
+ from astrbot.core.agent.handoff import HandoffTool
8
+ from astrbot.core.persona_mgr import PersonaManager
9
+ from astrbot.core.provider.func_tool_manager import FunctionToolManager
10
+
11
+
12
+ class SubAgentOrchestrator:
13
+ """Loads subagent definitions from config and registers handoff tools.
14
+
15
+ This is intentionally lightweight: it does not execute agents itself.
16
+ Execution happens via HandoffTool in FunctionToolExecutor.
17
+ """
18
+
19
+ def __init__(self, tool_mgr: FunctionToolManager, persona_mgr: PersonaManager):
20
+ self._tool_mgr = tool_mgr
21
+ self._persona_mgr = persona_mgr
22
+ self.handoffs: list[HandoffTool] = []
23
+
24
+ async def reload_from_config(self, cfg: dict[str, Any]) -> None:
25
+ from astrbot.core.astr_agent_context import AstrAgentContext
26
+
27
+ agents = cfg.get("agents", [])
28
+ if not isinstance(agents, list):
29
+ logger.warning("subagent_orchestrator.agents must be a list")
30
+ return
31
+
32
+ handoffs: list[HandoffTool] = []
33
+ for item in agents:
34
+ if not isinstance(item, dict):
35
+ continue
36
+ if not item.get("enabled", True):
37
+ continue
38
+
39
+ name = str(item.get("name", "")).strip()
40
+ if not name:
41
+ continue
42
+
43
+ persona_id = item.get("persona_id")
44
+ persona_data = None
45
+ if persona_id:
46
+ try:
47
+ persona_data = await self._persona_mgr.get_persona(persona_id)
48
+ except StopIteration:
49
+ logger.warning(
50
+ "SubAgent persona %s not found, fallback to inline prompt.",
51
+ persona_id,
52
+ )
53
+
54
+ instructions = str(item.get("system_prompt", "")).strip()
55
+ public_description = str(item.get("public_description", "")).strip()
56
+ provider_id = item.get("provider_id")
57
+ if provider_id is not None:
58
+ provider_id = str(provider_id).strip() or None
59
+ tools = item.get("tools", [])
60
+ begin_dialogs = None
61
+
62
+ if persona_data:
63
+ instructions = persona_data.system_prompt or instructions
64
+ begin_dialogs = persona_data.begin_dialogs
65
+ tools = persona_data.tools
66
+ if public_description == "" and persona_data.system_prompt:
67
+ public_description = persona_data.system_prompt[:120]
68
+ if tools is None:
69
+ tools = None
70
+ elif not isinstance(tools, list):
71
+ tools = []
72
+ else:
73
+ tools = [str(t).strip() for t in tools if str(t).strip()]
74
+
75
+ agent = Agent[AstrAgentContext](
76
+ name=name,
77
+ instructions=instructions,
78
+ tools=tools, # type: ignore
79
+ )
80
+ agent.begin_dialogs = begin_dialogs
81
+ # The tool description should be a short description for the main LLM,
82
+ # while the subagent system prompt can be longer/more specific.
83
+ handoff = HandoffTool(
84
+ agent=agent,
85
+ tool_description=public_description or None,
86
+ )
87
+
88
+ # Optional per-subagent chat provider override.
89
+ handoff.provider_id = provider_id
90
+
91
+ handoffs.append(handoff)
92
+
93
+ for handoff in handoffs:
94
+ logger.info(f"Registered subagent handoff tool: {handoff.name}")
95
+
96
+ self.handoffs = handoffs
@@ -0,0 +1,174 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic import Field
4
+ from pydantic.dataclasses import dataclass
5
+
6
+ from astrbot.core.agent.run_context import ContextWrapper
7
+ from astrbot.core.agent.tool import FunctionTool, ToolExecResult
8
+ from astrbot.core.astr_agent_context import AstrAgentContext
9
+
10
+
11
+ @dataclass
12
+ class CreateActiveCronTool(FunctionTool[AstrAgentContext]):
13
+ name: str = "create_future_task"
14
+ description: str = (
15
+ "Create a future task for your future. Supports recurring cron expressions or one-time run_at datetime. "
16
+ "Use this when you or the user want scheduled follow-up or proactive actions."
17
+ )
18
+ parameters: dict = Field(
19
+ default_factory=lambda: {
20
+ "type": "object",
21
+ "properties": {
22
+ "cron_expression": {
23
+ "type": "string",
24
+ "description": "Cron expression defining recurring schedule (e.g., '0 8 * * *').",
25
+ },
26
+ "run_at": {
27
+ "type": "string",
28
+ "description": "ISO datetime for one-time execution, e.g., 2026-02-02T08:00:00+08:00. Use with run_once=true.",
29
+ },
30
+ "note": {
31
+ "type": "string",
32
+ "description": "Detailed instructions for your future agent to execute when it wakes.",
33
+ },
34
+ "name": {
35
+ "type": "string",
36
+ "description": "Optional label to recognize this future task.",
37
+ },
38
+ "run_once": {
39
+ "type": "boolean",
40
+ "description": "If true, the task will run only once and then be deleted. Use run_at to specify the time.",
41
+ },
42
+ },
43
+ "required": ["note"],
44
+ }
45
+ )
46
+
47
+ async def call(
48
+ self, context: ContextWrapper[AstrAgentContext], **kwargs
49
+ ) -> ToolExecResult:
50
+ cron_mgr = context.context.context.cron_manager
51
+ if cron_mgr is None:
52
+ return "error: cron manager is not available."
53
+
54
+ cron_expression = kwargs.get("cron_expression")
55
+ run_at = kwargs.get("run_at")
56
+ run_once = bool(kwargs.get("run_once", False))
57
+ note = str(kwargs.get("note", "")).strip()
58
+ name = str(kwargs.get("name") or "").strip() or "active_agent_task"
59
+
60
+ if not note:
61
+ return "error: note is required."
62
+ if run_once and not run_at:
63
+ return "error: run_at is required when run_once=true."
64
+ if (not run_once) and not cron_expression:
65
+ return "error: cron_expression is required when run_once=false."
66
+ if run_once and cron_expression:
67
+ cron_expression = None
68
+ run_at_dt = None
69
+ if run_at:
70
+ try:
71
+ run_at_dt = datetime.fromisoformat(str(run_at))
72
+ except Exception:
73
+ return "error: run_at must be ISO datetime, e.g., 2026-02-02T08:00:00+08:00"
74
+
75
+ payload = {
76
+ "session": context.context.event.unified_msg_origin,
77
+ "sender_id": context.context.event.get_sender_id(),
78
+ "note": note,
79
+ "origin": "tool",
80
+ }
81
+
82
+ job = await cron_mgr.add_active_job(
83
+ name=name,
84
+ cron_expression=str(cron_expression) if cron_expression else None,
85
+ payload=payload,
86
+ description=note,
87
+ run_once=run_once,
88
+ run_at=run_at_dt,
89
+ )
90
+ next_run = job.next_run_time or run_at_dt
91
+ suffix = (
92
+ f"one-time at {next_run}"
93
+ if run_once
94
+ else f"expression '{cron_expression}' (next {next_run})"
95
+ )
96
+ return f"Scheduled future task {job.job_id} ({job.name}) {suffix}."
97
+
98
+
99
+ @dataclass
100
+ class DeleteCronJobTool(FunctionTool[AstrAgentContext]):
101
+ name: str = "delete_future_task"
102
+ description: str = "Delete a future task (cron job) by its job_id."
103
+ parameters: dict = Field(
104
+ default_factory=lambda: {
105
+ "type": "object",
106
+ "properties": {
107
+ "job_id": {
108
+ "type": "string",
109
+ "description": "The job_id returned when the job was created.",
110
+ }
111
+ },
112
+ "required": ["job_id"],
113
+ }
114
+ )
115
+
116
+ async def call(
117
+ self, context: ContextWrapper[AstrAgentContext], **kwargs
118
+ ) -> ToolExecResult:
119
+ cron_mgr = context.context.context.cron_manager
120
+ if cron_mgr is None:
121
+ return "error: cron manager is not available."
122
+ job_id = kwargs.get("job_id")
123
+ if not job_id:
124
+ return "error: job_id is required."
125
+ await cron_mgr.delete_job(str(job_id))
126
+ return f"Deleted cron job {job_id}."
127
+
128
+
129
+ @dataclass
130
+ class ListCronJobsTool(FunctionTool[AstrAgentContext]):
131
+ name: str = "list_future_tasks"
132
+ description: str = "List existing future tasks (cron jobs) for inspection."
133
+ parameters: dict = Field(
134
+ default_factory=lambda: {
135
+ "type": "object",
136
+ "properties": {
137
+ "job_type": {
138
+ "type": "string",
139
+ "description": "Optional filter: basic or active_agent.",
140
+ }
141
+ },
142
+ }
143
+ )
144
+
145
+ async def call(
146
+ self, context: ContextWrapper[AstrAgentContext], **kwargs
147
+ ) -> ToolExecResult:
148
+ cron_mgr = context.context.context.cron_manager
149
+ if cron_mgr is None:
150
+ return "error: cron manager is not available."
151
+ job_type = kwargs.get("job_type")
152
+ jobs = await cron_mgr.list_jobs(job_type)
153
+ if not jobs:
154
+ return "No cron jobs found."
155
+ lines = []
156
+ for j in jobs:
157
+ lines.append(
158
+ f"{j.job_id} | {j.name} | {j.job_type} | run_once={getattr(j, 'run_once', False)} | enabled={j.enabled} | next={j.next_run_time}"
159
+ )
160
+ return "\n".join(lines)
161
+
162
+
163
+ CREATE_CRON_JOB_TOOL = CreateActiveCronTool()
164
+ DELETE_CRON_JOB_TOOL = DeleteCronJobTool()
165
+ LIST_CRON_JOBS_TOOL = ListCronJobsTool()
166
+
167
+ __all__ = [
168
+ "CREATE_CRON_JOB_TOOL",
169
+ "DELETE_CRON_JOB_TOOL",
170
+ "LIST_CRON_JOBS_TOOL",
171
+ "CreateActiveCronTool",
172
+ "DeleteCronJobTool",
173
+ "ListCronJobsTool",
174
+ ]
@@ -0,0 +1,31 @@
1
+ import json
2
+
3
+ from astrbot import logger
4
+ from astrbot.core.conversation_mgr import ConversationManager
5
+ from astrbot.core.platform.astr_message_event import AstrMessageEvent
6
+ from astrbot.core.provider.entities import ProviderRequest
7
+
8
+
9
+ async def persist_agent_history(
10
+ conversation_manager: ConversationManager,
11
+ *,
12
+ event: AstrMessageEvent,
13
+ req: ProviderRequest,
14
+ summary_note: str,
15
+ ) -> None:
16
+ """Persist agent interaction into conversation history."""
17
+ if not req or not req.conversation:
18
+ return
19
+
20
+ history = []
21
+ try:
22
+ history = json.loads(req.conversation.history or "[]")
23
+ except Exception as exc: # noqa: BLE001
24
+ logger.warning("Failed to parse conversation history: %s", exc)
25
+ history.append({"role": "user", "content": "Output your last task result below."})
26
+ history.append({"role": "assistant", "content": summary_note})
27
+ await conversation_manager.update_conversation(
28
+ event.unified_msg_origin,
29
+ req.conversation.cid,
30
+ history=history,
31
+ )
@@ -50,6 +50,10 @@ class TraceSpan:
50
50
  self.started_at = time.time()
51
51
 
52
52
  def record(self, action: str, **fields: Any) -> None:
53
+ # Check if trace recording is enabled
54
+ if not astrbot_config.get("trace_enable", True):
55
+ return
56
+
53
57
  payload = {
54
58
  "type": "trace",
55
59
  "level": "TRACE",
@@ -5,6 +5,7 @@ from .chatui_project import ChatUIProjectRoute
5
5
  from .command import CommandRoute
6
6
  from .config import ConfigRoute
7
7
  from .conversation import ConversationRoute
8
+ from .cron import CronRoute
8
9
  from .file import FileRoute
9
10
  from .knowledge_base import KnowledgeBaseRoute
10
11
  from .log import LogRoute
@@ -15,6 +16,7 @@ from .session_management import SessionManagementRoute
15
16
  from .skills import SkillsRoute
16
17
  from .stat import StatRoute
17
18
  from .static_file import StaticFileRoute
19
+ from .subagent import SubAgentRoute
18
20
  from .tools import ToolsRoute
19
21
  from .update import UpdateRoute
20
22
 
@@ -26,6 +28,7 @@ __all__ = [
26
28
  "CommandRoute",
27
29
  "ConfigRoute",
28
30
  "ConversationRoute",
31
+ "CronRoute",
29
32
  "FileRoute",
30
33
  "KnowledgeBaseRoute",
31
34
  "LogRoute",
@@ -35,6 +38,7 @@ __all__ = [
35
38
  "SessionManagementRoute",
36
39
  "StatRoute",
37
40
  "StaticFileRoute",
41
+ "SubAgentRoute",
38
42
  "ToolsRoute",
39
43
  "SkillsRoute",
40
44
  "UpdateRoute",