From 209c51387e4e1f7c63db66a2f8d015a3ee92d6eb Mon Sep 17 00:00:00 2001 From: "F. Abyssalis" Date: Sat, 6 Jun 2026 09:22:00 +0800 Subject: [PATCH 1/5] Refactor system prompt handling with XML-like wrapping --- astrbot/core/astr_main_agent.py | 164 ++++++++++++++++++++++---------- 1 file changed, 113 insertions(+), 51 deletions(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 1c4fd400a0..be0cf28354 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -238,6 +238,21 @@ async def _get_session_conv( return conversation + + +def _wrap_system_block(name: str, content: str | None) -> str: + """Wrap a system prompt fragment with clear XML-like boundaries. + + This keeps persona, skills, runtime, and tool-use instructions visually + separated even though the provider receives them as one system prompt. + """ + if not content: + return "" + content = str(content).strip() + if not content: + return "" + return f"\n<{name}>\n{content}\n\n" + async def _apply_kb( event: AstrMessageEvent, req: ProviderRequest, @@ -256,8 +271,9 @@ async def _apply_kb( if not kb_result: return if req.system_prompt is not None: - req.system_prompt += ( - f"\n\n[Related Knowledge Base Results]:\n{kb_result}" + req.system_prompt += _wrap_system_block( + "related_knowledge_base_results", + kb_result, ) except Exception as exc: # noqa: BLE001 logger.error("Error occurred while retrieving knowledge base: %s", exc) @@ -359,12 +375,14 @@ def _apply_workspace_extra_prompt( if not extra_prompt: return - req.system_prompt = ( - f"{req.system_prompt or ''}\n" - "[Workspace Extra Prompt]\n" - "The following instructions are loaded from the current workspace " - "`EXTRA_PROMPT.md` file.\n" - f"{extra_prompt}\n" + req.system_prompt = f"{req.system_prompt or ''}" + req.system_prompt += _wrap_system_block( + "workspace_extra_prompt", + ( + "The following instructions are loaded from the current workspace " + "`EXTRA_PROMPT.md` file.\n" + f"{extra_prompt}" + ), ) @@ -378,7 +396,11 @@ def _apply_local_env_tools(req: ProviderRequest, plugin_context: Context) -> Non req.func_tool.add_tool(tool_mgr.get_builtin_tool(FileWriteTool)) req.func_tool.add_tool(tool_mgr.get_builtin_tool(FileEditTool)) req.func_tool.add_tool(tool_mgr.get_builtin_tool(GrepTool)) - req.system_prompt = f"{req.system_prompt or ''}\n{_build_local_mode_prompt()}\n" + req.system_prompt = f"{req.system_prompt or ''}" + req.system_prompt += _wrap_system_block( + "runtime_environment", + _build_local_mode_prompt(), + ) def _build_local_mode_prompt() -> str: @@ -460,11 +482,17 @@ async def _ensure_persona_and_skills( if persona: # Inject persona system prompt if prompt := persona["prompt"]: - req.system_prompt += f"\n# Persona Instructions\n\n{prompt}\n" + req.system_prompt += _wrap_system_block( + "persona_instructions", + prompt, + ) if begin_dialogs := copy.deepcopy(persona.get("_begin_dialogs_processed")): req.contexts[:0] = begin_dialogs elif use_webchat_special_default: - req.system_prompt += CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT + req.system_prompt += _wrap_system_block( + "persona_instructions", + CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT, + ) # Inject skills prompt runtime = cfg.get("computer_use_runtime", "local") @@ -480,12 +508,18 @@ async def _ensure_persona_and_skills( allowed = set(persona["skills"]) skills = [skill for skill in skills if skill.name in allowed] if skills: - req.system_prompt += f"\n{build_skills_prompt(skills)}\n" + req.system_prompt += _wrap_system_block( + "available_skills", + build_skills_prompt(skills), + ) if runtime == "none": - req.system_prompt += ( - "User has not enabled the Computer Use feature. " - "You cannot use shell or Python to perform skills. " - "If you need to use these capabilities, ask the user to enable Computer Use in the AstrBot WebUI -> Config." + req.system_prompt += _wrap_system_block( + "computer_use_disabled_notice", + ( + "User has not enabled the Computer Use feature. " + "You cannot use shell or Python to perform skills. " + "If you need to use these capabilities, ask the user to enable Computer Use in the AstrBot WebUI -> Config." + ), ) tmgr = plugin_context.get_llm_tool_manager() @@ -567,7 +601,10 @@ async def _ensure_persona_and_skills( .get("router_system_prompt", "") ).strip() if router_prompt: - req.system_prompt += f"\n{router_prompt}\n" + req.system_prompt += _wrap_system_block( + "subagent_routing_instructions", + router_prompt, + ) try: event.trace.record( "sel_persona", @@ -1004,7 +1041,13 @@ async def _handle_webchat( def _apply_llm_safety_mode(config: MainAgentBuildConfig, req: ProviderRequest) -> None: if config.safety_mode_strategy == "system_prompt": - req.system_prompt = f"{LLM_SAFETY_MODE_SYSTEM_PROMPT}\n\n{req.system_prompt}" + req.system_prompt = ( + _wrap_system_block( + "safety_instructions", + LLM_SAFETY_MODE_SYSTEM_PROMPT, + ) + + f"\n{req.system_prompt or ''}" + ) else: logger.warning( "Unsupported llm_safety_mode strategy: %s.", @@ -1043,23 +1086,27 @@ def _apply_sandbox_tools( if booter == "shipyard_neo": # Neo-specific path rule: filesystem tools operate relative to sandbox # workspace root. Do not prepend "/workspace". - req.system_prompt += ( - "\n[Shipyard Neo File Path Rule]\n" - "When using sandbox filesystem tools (upload/download/read/write/list/delete), " - "always pass paths relative to the sandbox workspace root. " - "Example: use `baidu_homepage.png` instead of `/workspace/baidu_homepage.png`.\n" + req.system_prompt += _wrap_system_block( + "shipyard_neo_file_path_rule", + ( + "When using sandbox filesystem tools (upload/download/read/write/list/delete), " + "always pass paths relative to the sandbox workspace root. " + "Example: use `baidu_homepage.png` instead of `/workspace/baidu_homepage.png`." + ), ) - req.system_prompt += ( - "\n[Neo Skill Lifecycle Workflow]\n" - "When user asks to create/update a reusable skill in Neo mode, use lifecycle tools instead of directly writing local skill folders.\n" - "Preferred sequence:\n" - "1) Use `astrbot_create_skill_payload` to store canonical payload content and get `payload_ref`.\n" - "2) Use `astrbot_create_skill_candidate` with `skill_key` + `source_execution_ids` (and optional `payload_ref`) to create a candidate.\n" - "3) Use `astrbot_promote_skill_candidate` to release: `stage=canary` for trial; `stage=stable` for production.\n" - "For stable release, set `sync_to_local=true` to sync `payload.skill_markdown` into local `SKILL.md`.\n" - "Do not treat ad-hoc generated files as reusable Neo skills unless they are captured via payload/candidate/release.\n" - "To update an existing skill, create a new payload/candidate and promote a new release version; avoid patching old local folders directly.\n" + req.system_prompt += _wrap_system_block( + "neo_skill_lifecycle_workflow", + ( + "When user asks to create/update a reusable skill in Neo mode, use lifecycle tools instead of directly writing local skill folders.\n" + "Preferred sequence:\n" + "1) Use `astrbot_create_skill_payload` to store canonical payload content and get `payload_ref`.\n" + "2) Use `astrbot_create_skill_candidate` with `skill_key` + `source_execution_ids` (and optional `payload_ref`) to create a candidate.\n" + "3) Use `astrbot_promote_skill_candidate` to release: `stage=canary` for trial; `stage=stable` for production.\n" + "For stable release, set `sync_to_local=true` to sync `payload.skill_markdown` into local `SKILL.md`.\n" + "Do not treat ad-hoc generated files as reusable Neo skills unless they are captured via payload/candidate/release.\n" + "To update an existing skill, create a new payload/candidate and promote a new release version; avoid patching old local folders directly." + ), ) # Determine sandbox capabilities from an already-booted session. @@ -1093,22 +1140,28 @@ def _apply_sandbox_tools( req.func_tool.add_tool(tool_mgr.get_builtin_tool(SyncSkillReleaseTool)) if booter == "cua": - req.system_prompt += ( - "\n[CUA Desktop Control]\n" - "Use `astrbot_execute_shell` with `background=true` to launch GUI apps. " - 'Use Firefox for browser tasks, for example `firefox "https://example.com"`. ' - "After each visible step, call `astrbot_cua_screenshot` with " - "`send_to_user=true` and `return_image_to_llm=true` so the user can " - "monitor progress. When typing, inspect the screenshot first and confirm " - "the target field is focused and empty or safe to append to. Use " - "`astrbot_cua_mouse_click` for coordinates and `astrbot_cua_keyboard_type` " - "for text input; use text=`\\n` for Enter.\n" + req.system_prompt += _wrap_system_block( + "cua_desktop_control_instructions", + ( + "Use `astrbot_execute_shell` with `background=true` to launch GUI apps. " + 'Use Firefox for browser tasks, for example `firefox "https://example.com"`. ' + "After each visible step, call `astrbot_cua_screenshot` with " + "`send_to_user=true` and `return_image_to_llm=true` so the user can " + "monitor progress. When typing, inspect the screenshot first and confirm " + "the target field is focused and empty or safe to append to. Use " + "`astrbot_cua_mouse_click` for coordinates and `astrbot_cua_keyboard_type` " + "for text input; use text=`\\n` for Enter." + ), ) req.func_tool.add_tool(tool_mgr.get_builtin_tool(CuaScreenshotTool)) req.func_tool.add_tool(tool_mgr.get_builtin_tool(CuaMouseClickTool)) req.func_tool.add_tool(tool_mgr.get_builtin_tool(CuaKeyboardTypeTool)) - req.system_prompt = f"{req.system_prompt or ''}\n{SANDBOX_MODE_PROMPT}\n" + req.system_prompt = f"{req.system_prompt or ''}" + req.system_prompt += _wrap_system_block( + "sandbox_mode_instructions", + SANDBOX_MODE_PROMPT, + ) def _proactive_cron_job_tools(req: ProviderRequest, plugin_context: Context) -> None: @@ -1507,18 +1560,27 @@ async def build_main_agent( ) if config.computer_use_runtime == "local": - tool_prompt += ( - f"\nCurrent workspace you can use: " - f"`{_get_workspace_path_for_umo(event.unified_msg_origin)}`\n" - "Unless the user explicitly specifies a different directory, " - "perform all file-related operations in this workspace.\n" + req.system_prompt += _wrap_system_block( + "workspace_runtime_rule", + ( + f"Current workspace you can use: " + f"`{_get_workspace_path_for_umo(event.unified_msg_origin)}`\n" + "Unless the user explicitly specifies a different directory, " + "perform all file-related operations in this workspace." + ), ) - req.system_prompt += f"\n{tool_prompt}\n" + req.system_prompt += _wrap_system_block( + "tool_use_instructions", + tool_prompt, + ) action_type = event.get_extra("action_type") if action_type == "live": - req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n" + req.system_prompt += _wrap_system_block( + "live_mode_instructions", + LIVE_MODE_SYSTEM_PROMPT, + ) reset_coro = agent_runner.reset( provider=provider, From 561b0fcbf5f0b921d94cadebbf25ed2a50eb6aa1 Mon Sep 17 00:00:00 2001 From: "F. Abyssalis" Date: Sat, 6 Jun 2026 09:43:43 +0800 Subject: [PATCH 2/5] Clean up blank lines Removed unnecessary blank lines in the astr_main_agent.py file. --- astrbot/core/astr_main_agent.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index be0cf28354..791c904d61 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -238,8 +238,6 @@ async def _get_session_conv( return conversation - - def _wrap_system_block(name: str, content: str | None) -> str: """Wrap a system prompt fragment with clear XML-like boundaries. @@ -253,6 +251,7 @@ def _wrap_system_block(name: str, content: str | None) -> str: return "" return f"\n<{name}>\n{content}\n\n" + async def _apply_kb( event: AstrMessageEvent, req: ProviderRequest, From 15be3370a1b6f5f3493e5dbb03e93fca4fa764c9 Mon Sep 17 00:00:00 2001 From: "F. Abyssalis" Date: Sat, 6 Jun 2026 09:55:02 +0800 Subject: [PATCH 3/5] Fix indentation in system prompt wrapping --- astrbot/core/astr_main_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 791c904d61..60acdf78c2 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -1044,7 +1044,7 @@ def _apply_llm_safety_mode(config: MainAgentBuildConfig, req: ProviderRequest) - _wrap_system_block( "safety_instructions", LLM_SAFETY_MODE_SYSTEM_PROMPT, - ) + ).lstrip() + f"\n{req.system_prompt or ''}" ) else: From a6b32477ca1a93c83e727fcd629e32d75c89f5f0 Mon Sep 17 00:00:00 2001 From: "F. Abyssalis" Date: Sat, 6 Jun 2026 09:58:12 +0800 Subject: [PATCH 4/5] Update tests for structured system prompt blocks --- tests/unit/test_astr_main_agent.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_astr_main_agent.py b/tests/unit/test_astr_main_agent.py index db729a23ba..8987814417 100644 --- a/tests/unit/test_astr_main_agent.py +++ b/tests/unit/test_astr_main_agent.py @@ -343,8 +343,9 @@ async def test_apply_kb_without_agentic_mode(self, mock_event, mock_context): ): await module._apply_kb(mock_event, req, mock_context, config) - assert "[Related Knowledge Base Results]:" in req.system_prompt + assert "" in req.system_prompt assert "KB result" in req.system_prompt + assert "" in req.system_prompt @pytest.mark.asyncio async def test_apply_kb_with_agentic_mode(self, mock_event, mock_context): @@ -1788,7 +1789,9 @@ def test_apply_llm_safety_mode_prepends_safety_prompt(self): module._apply_llm_safety_mode(config, req) - assert req.system_prompt.startswith("You are running in Safe Mode") + assert req.system_prompt.startswith("") + assert "You are running in Safe Mode" in req.system_prompt + assert "" in req.system_prompt assert "My custom prompt" in req.system_prompt def test_apply_llm_safety_mode_with_none_system_prompt(self): From 39f925c11193f80b915b07466508b8411c394b4c Mon Sep 17 00:00:00 2001 From: "F. Abyssalis" Date: Sat, 6 Jun 2026 10:13:28 +0800 Subject: [PATCH 5/5] Refactor system_prompt assignment for clarity --- astrbot/core/astr_main_agent.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 60acdf78c2..8bb8f801a0 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -374,8 +374,7 @@ def _apply_workspace_extra_prompt( if not extra_prompt: return - req.system_prompt = f"{req.system_prompt or ''}" - req.system_prompt += _wrap_system_block( + req.system_prompt = (req.system_prompt or "") + _wrap_system_block( "workspace_extra_prompt", ( "The following instructions are loaded from the current workspace " @@ -1559,7 +1558,7 @@ async def build_main_agent( ) if config.computer_use_runtime == "local": - req.system_prompt += _wrap_system_block( + req.system_prompt = (req.system_prompt or "") + _wrap_system_block( "workspace_runtime_rule", ( f"Current workspace you can use: " @@ -1569,14 +1568,14 @@ async def build_main_agent( ), ) - req.system_prompt += _wrap_system_block( + req.system_prompt = (req.system_prompt or "") + _wrap_system_block( "tool_use_instructions", tool_prompt, ) action_type = event.get_extra("action_type") if action_type == "live": - req.system_prompt += _wrap_system_block( + req.system_prompt = (req.system_prompt or "") + _wrap_system_block( "live_mode_instructions", LIVE_MODE_SYSTEM_PROMPT, )