"""Subagent manager for background task execution with progress tracking."""

import asyncio
import json
import re
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal

from loguru import logger

from bao.agent import shared
from bao.agent.artifacts import ArtifactStore, apply_tool_output_budget
from bao.agent.capability_registry import build_available_tool_lines
from bao.agent.memory import DEFAULT_MEMORY_POLICY, MemoryPolicy
from bao.agent.protocol import ToolErrorCategory
from bao.agent.run_artifacts import build_run_artifact_payload
from bao.agent.run_controller import RunLoopState, apply_pre_iteration_checks, build_error_feedback
from bao.agent.tool_exposure import ToolExposureSnapshot
from bao.agent.tool_result import tool_result_payload
from bao.agent.tools.diagnostics import RuntimeDiagnosticsTool
from bao.agent.tools.filesystem import EditFileTool, ListDirTool, ReadFileTool, WriteFileTool
from bao.agent.tools.registry import ToolRegistry
from bao.agent.tools.shell import ExecTool
from bao.agent.tools.web import WebFetchTool, WebSearchTool
from bao.bus.events import ControlEvent, OutboundMessage
from bao.bus.queue import MessageBus
from bao.providers.base import LLMProvider
from bao.runtime_diagnostics import get_runtime_diagnostics_store

if TYPE_CHECKING:
    from bao.config.schema import ExecToolConfig, WebSearchConfig
    from bao.session.manager import SessionManager


_SUBAGENT_ERROR_KEYWORDS = ("error:", "traceback", "failed", "exception", "permission denied")
_ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")

SpawnResultStatus = Literal["spawned", "failed"]


@dataclass
class TaskStatus:
    task_id: str
    label: str
    task_description: str
    origin: dict[str, str]
    child_session_key: str | None = None
    status: str = "running"  # running | completed | failed | cancelled
    iteration: int = 0
    max_iterations: int = 20
    tool_steps: int = 0
    phase: str = "starting"  # starting | thinking | tool:<name> | summarizing | cancel_requested
    started_at: float = field(default_factory=time.time)
    updated_at: float = field(default_factory=time.time)
    result_summary: str | None = None
    resume_context: str | None = None
    offloaded_count: int = 0
    offloaded_chars: int = 0
    clipped_count: int = 0
    clipped_chars: int = 0
    recent_actions: list[str] = field(default_factory=list)  # last N tool call summaries
    last_error_category: str | None = None
    last_error_code: str | None = None
    last_error_message: str | None = None


@dataclass(frozen=True)
class SpawnIssue:
    code: str
    message: str

    def to_payload(self) -> dict[str, str]:
        return {"code": self.code, "message": self.message}


@dataclass(frozen=True)
class SpawnTaskRef:
    task_id: str
    label: str
    status: str = "running"
    child_session_key: str | None = None

    def to_payload(self) -> dict[str, str]:
        payload = {
            "task_id": self.task_id,
            "label": self.label,
            "status": self.status,
        }
        if self.child_session_key:
            payload["child_session_key"] = self.child_session_key
        return payload


@dataclass(frozen=True)
class SpawnResult:
    status: SpawnResultStatus
    message: str
    task: SpawnTaskRef | None = None
    warning: SpawnIssue | None = None
    error: SpawnIssue | None = None
    schema_version: int = 1

    @classmethod
    def spawned(
        cls,
        *,
        task_id: str,
        label: str,
        child_session_key: str | None,
        warning: SpawnIssue | None = None,
    ) -> "SpawnResult":
        return cls(
            status="spawned",
            message=(
                "Subagent spawned. Query progress with task.task_id via check_tasks or "
                "check_tasks_json."
            ),
            task=SpawnTaskRef(
                task_id=task_id,
                label=label,
                child_session_key=child_session_key,
            ),
            warning=warning,
        )

    @classmethod
    def failed(cls, *, code: str, message: str) -> "SpawnResult":
        return cls(
            status="failed",
            message="Subagent spawn failed.",
            error=SpawnIssue(code=code, message=message),
        )

    def to_payload(self) -> dict[str, Any]:
        return {
            "schema_version": self.schema_version,
            "status": self.status,
            "message": self.message,
            "task": self.task.to_payload() if self.task else None,
            "warning": self.warning.to_payload() if self.warning else None,
            "error": self.error.to_payload() if self.error else None,
        }


class SubagentManager:
    _MAX_COMPLETED: int = 50  # retain at most N finished tasks
    _PROGRESS_INTERVAL: int = 5  # push progress every N iterations

    def __init__(
        self,
        provider: LLMProvider,
        workspace: Path,
        bus: MessageBus,
        model: str | None = None,
        temperature: float = 0.7,
        max_tokens: int = 4096,
        reasoning_effort: str | None = None,
        service_tier: str | None = None,
        search_config: "WebSearchConfig | None" = None,
        web_proxy: str | None = None,
        exec_config: "ExecToolConfig | None" = None,
        restrict_to_workspace: bool = False,
        max_iterations: int = 20,
        context_management: str = "auto",
        tool_output_offload_chars: int = 8000,
        tool_output_preview_chars: int = 3000,
        tool_output_hard_chars: int = 6000,
        context_compact_bytes_est: int = 240000,
        context_compact_keep_recent_tool_blocks: int = 4,
        artifact_retention_days: int = 7,
        memory_store: Any | None = None,
        memory_policy: MemoryPolicy | None = None,
        image_generation_config: Any | None = None,
        desktop_config: Any | None = None,
        browser_enabled: bool = True,
        sessions: "SessionManager | None" = None,
        utility_provider: LLMProvider | None = None,
        utility_model: str | None = None,
        experience_mode: str = "utility",
    ):
        from bao.config.schema import ExecToolConfig

        self.provider = provider
        self.workspace = workspace
        self.bus = bus
        self.model = model or provider.get_default_model()
        self.temperature = temperature
        self.max_tokens = max_tokens
        self.reasoning_effort = reasoning_effort
        self.service_tier = service_tier
        self.search_config = search_config
        self.web_proxy = web_proxy
        self.exec_config = exec_config or ExecToolConfig()
        self.restrict_to_workspace = restrict_to_workspace
        self.image_generation_config = image_generation_config
        self.desktop_config = desktop_config
        self.browser_enabled = browser_enabled
        self.sessions = sessions
        self.max_iterations = max(1, int(max_iterations))
        self._ctx_mgmt = context_management
        self._tool_offload_chars = max(1, int(tool_output_offload_chars))
        self._tool_preview_chars = max(0, int(tool_output_preview_chars))
        self._tool_hard_chars = max(500, int(tool_output_hard_chars))
        self._compact_bytes = max(50000, int(context_compact_bytes_est))
        self._compact_keep_blocks = max(1, int(context_compact_keep_recent_tool_blocks))
        self._artifact_retention_days = max(1, int(artifact_retention_days))
        self._memory = memory_store
        self._memory_policy = memory_policy or DEFAULT_MEMORY_POLICY
        self._utility_provider = utility_provider
        self._utility_model = utility_model
        self._experience_mode = experience_mode.lower()
        self._artifact_cleanup_done = False
        self._runtime_diagnostics = get_runtime_diagnostics_store()
        self._running_tasks: dict[str, asyncio.Task[None]] = {}
        self._task_statuses: dict[str, TaskStatus] = {}
        self._session_tasks: dict[str, set[str]] = {}  # session_key -> {task_id, ...}

    def set_aux_runtime(
        self,
        *,
        utility_provider: LLMProvider | None,
        utility_model: str | None,
        experience_mode: str,
    ) -> None:
        self._utility_provider = utility_provider
        self._utility_model = utility_model
        self._experience_mode = (experience_mode or "utility").lower()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def spawn(
        self,
        task: str,
        label: str | None = None,
        origin_channel: str = "gateway",
        origin_chat_id: str = "direct",
        session_key: str | None = None,
        context_from: str | None = None,
        child_session_key: str | None = None,
    ) -> SpawnResult:
        task_id = uuid.uuid4().hex[:12]
        while task_id in self._task_statuses or task_id in self._running_tasks:
            task_id = uuid.uuid4().hex[:12]
        display_label = label or task or "unnamed task"
        safe_label = self._sanitize_visible(display_label).replace('"', "'")
        if len(safe_label) > 48:
            safe_label = safe_label[:48] + "…"
        origin = {"channel": origin_channel, "chat_id": origin_chat_id}
        if session_key:
            origin["session_key"] = session_key
        child_key_error = self._validate_explicit_child_session_key(
            parent_session_key=session_key,
            child_session_key=child_session_key,
        )
        if child_key_error:
            return SpawnResult.failed(code=child_key_error.code, message=child_key_error.message)
        resolved_child_session_key = self._resolve_child_session_key(
            parent_session_key=session_key,
            child_session_key=child_session_key,
            lineage_id=task_id,
        )
        if resolved_child_session_key:
            origin["child_session_key"] = resolved_child_session_key

        resume_context, warning = self._resolve_resume_context(context_from)

        self._task_statuses[task_id] = TaskStatus(
            task_id=task_id,
            child_session_key=resolved_child_session_key,
            label=safe_label,
            task_description=task[:200],
            origin=origin,
            max_iterations=self.max_iterations,
            resume_context=resume_context,
        )
        self._cleanup_completed()

        bg_task = asyncio.create_task(
            self._run_subagent(task_id, task, safe_label, origin, context_from)
        )
        self._running_tasks[task_id] = bg_task
        if session_key:
            self._session_tasks.setdefault(session_key, set()).add(task_id)

        def _cleanup(_: asyncio.Task[None]) -> None:
            self._running_tasks.pop(task_id, None)
            if session_key and (ids := self._session_tasks.get(session_key)):
                ids.discard(task_id)
                if not ids:
                    self._session_tasks.pop(session_key, None)

        bg_task.add_done_callback(_cleanup)

        logger.info("🚀 启动子代 / subagent spawned: [{}]: {}", task_id, safe_label)
        return SpawnResult.spawned(
            task_id=task_id,
            label=safe_label,
            child_session_key=resolved_child_session_key,
            warning=warning,
        )

    def get_task_status(self, task_id: str) -> TaskStatus | None:
        return self._task_statuses.get(task_id)

    def get_all_statuses(self) -> list[TaskStatus]:
        return list(self._task_statuses.values())

    def get_running_count(self) -> int:
        return len(self._running_tasks)

    async def _cancel_by_session(self, session_key: str, *, wait: bool) -> int:
        task_ids = list(self._session_tasks.get(session_key, []))  # snapshot
        tasks = [
            self._running_tasks[tid]
            for tid in task_ids
            if tid in self._running_tasks and not self._running_tasks[tid].done()
        ]
        for t in tasks:
            t.cancel()
        if wait and tasks:
            await asyncio.gather(*tasks, return_exceptions=True)
        return len(tasks)

    async def cancel_by_session(self, session_key: str, *, wait: bool = True) -> int:
        """Cancel all subagents for the given session. Returns count cancelled."""
        return await self._cancel_by_session(session_key, wait=wait)

    async def cancel_task(self, task_id: str) -> str:
        at = self._running_tasks.get(task_id)
        if not at:
            return f"No running task with id '{task_id}'."
        st = self._task_statuses.get(task_id)
        if at.done() or (st and st.status == "cancelled"):
            status = st.status if st else "finished"
            return f"Task '{task_id}' is already {status}."

        at.cancel()

        if st and st.status == "running":
            st.phase = "cancel_requested"
            st.updated_at = time.time()

        logger.info("👋 取消子代请求 / subagent cancel requested: [{}]", task_id)
        return f"Cancellation requested for task '{task_id}'."

    # ------------------------------------------------------------------
    # Internal: status helpers
    # ------------------------------------------------------------------

    _MAX_RECENT_ACTIONS: int = 6  # keep last N tool call summaries

    @staticmethod
    def _sanitize_visible(text: str) -> str:
        return shared.sanitize_visible_text(text)

    @staticmethod
    def _build_resume_context(context_from: str, prev: TaskStatus) -> str:
        return (
            f"[Continuing from previous task ({context_from})]\n"
            f"Previous task: {prev.task_description[:200]}\n"
            f"Previous result: {prev.result_summary or 'no summary'}"
        )

    def _resolve_resume_context(
        self, context_from: str | None
    ) -> tuple[str | None, SpawnIssue | None]:
        if not context_from:
            return None, None
        prev = self.get_task_status(context_from)
        if prev and prev.status in ("completed", "failed"):
            return self._build_resume_context(context_from, prev), None
        visible_context_from = self._sanitize_visible(context_from)
        return (
            None,
            SpawnIssue(
                code="context_from_unavailable",
                message=(
                    f"context_from={visible_context_from} not found or not finished; "
                    "resume context not injected."
                ),
            ),
        )

    @staticmethod
    def _child_session_family_key(parent_session_key: str) -> str:
        return f"subagent:{parent_session_key}"

    @classmethod
    def _build_child_session_key(cls, parent_session_key: str, lineage_id: str) -> str:
        return f"{cls._child_session_family_key(parent_session_key)}::{lineage_id}"

    def _resolve_child_session_key(
        self,
        *,
        parent_session_key: str | None,
        child_session_key: str | None,
        lineage_id: str,
    ) -> str | None:
        if isinstance(child_session_key, str) and child_session_key.strip():
            return child_session_key.strip()
        if not isinstance(parent_session_key, str) or not parent_session_key:
            return None
        return self._build_child_session_key(parent_session_key, lineage_id)

    def _validate_explicit_child_session_key(
        self,
        *,
        parent_session_key: str | None,
        child_session_key: str | None,
    ) -> SpawnIssue | None:
        if not isinstance(child_session_key, str) or not child_session_key.strip():
            return None
        if self.sessions is None:
            return None
        if not isinstance(parent_session_key, str) or not parent_session_key:
            return SpawnIssue(
                code="child_session_parent_required",
                message="child_session_key requires a parent session",
            )
        candidate = child_session_key.strip()
        if not self.sessions.session_exists(candidate):
            return SpawnIssue(
                code="unknown_child_session_key",
                message=f"unknown child_session_key '{candidate}'",
            )
        session = self.sessions.get_or_create(candidate)
        if session.metadata.get("parent_session_key") != parent_session_key:
            return SpawnIssue(
                code="child_session_parent_mismatch",
                message="child_session_key does not belong to this parent session",
            )
        return None

    def _persist_child_user_turn(
        self,
        child_session_key: str,
        *,
        parent_session_key: str,
        label: str,
        task_id: str,
        task: str,
    ) -> None:
        if self.sessions is None:
            return
        session = self.sessions.get_or_create(child_session_key)
        session.metadata.update(self._child_session_metadata(parent_session_key, label))
        session.add_message("user", task)
        self.sessions.save(session)
        self.sessions.set_child_running(child_session_key, task_id)

    @staticmethod
    def _child_session_metadata(parent_session_key: str, label: str) -> dict[str, Any]:
        return {
            "title": label,
            "session_kind": "subagent_child",
            "read_only": True,
            "parent_session_key": parent_session_key,
            "task_label": label,
        }

    def _persist_child_result(
        self,
        child_session_key: str,
        *,
        parent_session_key: str,
        label: str,
        task_id: str,
        result: str,
        status: str,
    ) -> None:
        if self.sessions is None:
            return
        session = self.sessions.get_or_create(child_session_key)
        self.sessions.clear_child_running(child_session_key, emit_change=False)
        session.metadata.update(
            self._child_session_metadata(parent_session_key, label)
            | {
                "child_status": status,
                "last_result_summary": self._sanitize_visible(result),
            }
        )
        assistant_status = "done" if status == "completed" else "error"
        session.add_message("assistant", result, status=assistant_status)
        self.sessions.save(session)

    def _child_session_history(self, child_session_key: str) -> list[dict[str, Any]]:
        if self.sessions is None or not self.sessions.session_exists(child_session_key):
            return []
        session = self.sessions.get_or_create(child_session_key)
        history = session.get_history(max_messages=80)
        return [
            dict(message) for message in history if message.get("role") in {"user", "assistant"}
        ]

    @staticmethod
    def _redact_tool_args_for_log(tool_name: str, args: dict[str, Any]) -> str:
        if tool_name == "exec":
            redacted = dict(args)
            command = redacted.get("command")
            if isinstance(command, str):
                redacted["command"] = f"<redacted:{len(command)} chars>"
            return json.dumps(redacted, ensure_ascii=False)

        if tool_name not in {"write_file", "edit_file"}:
            return json.dumps(args, ensure_ascii=False)

        redacted = dict(args)
        for key in ("content", "old_text", "new_text"):
            value = redacted.get(key)
            if isinstance(value, str):
                redacted[key] = f"<redacted:{len(value)} chars>"
        return json.dumps(redacted, ensure_ascii=False)

    @staticmethod
    def _normalize_progress_line(text: str) -> str:
        cleaned = _ANSI_ESCAPE_RE.sub("", text)
        cleaned = cleaned.replace("\x1b", "")
        cleaned = SubagentManager._sanitize_visible(cleaned).strip()
        if len(cleaned) > 180:
            cleaned = cleaned[:177] + "..."
        return cleaned

    def _update_status(
        self,
        task_id: str,
        *,
        iteration: int | None = None,
        phase: str | None = None,
        tool_steps: int | None = None,
        status: str | None = None,
        result_summary: str | None = None,
        action: str | None = None,
    ) -> None:
        st = self._task_statuses.get(task_id)
        if not st:
            return
        if iteration is not None:
            st.iteration = iteration
        if phase is not None:
            st.phase = phase
        if tool_steps is not None:
            st.tool_steps = tool_steps
        if status is not None:
            st.status = status
        if result_summary is not None:
            st.result_summary = self._sanitize_visible(result_summary)
        if action is not None:
            st.recent_actions.append(self._sanitize_visible(action))
            if len(st.recent_actions) > self._MAX_RECENT_ACTIONS:
                st.recent_actions = st.recent_actions[-self._MAX_RECENT_ACTIONS :]
        st.updated_at = time.time()

    def _record_runtime_diagnostic(
        self,
        *,
        stage: str,
        message: str,
        code: str = "",
        retryable: bool | None = None,
        task_id: str = "",
        label: str = "",
        details: dict[str, Any] | None = None,
    ) -> None:
        self._runtime_diagnostics.record_event(
            source="subagent",
            stage=stage,
            message=message,
            code=code,
            retryable=retryable,
            session_key=task_id,
            details={
                "task_id": task_id,
                "label": self._sanitize_visible(label),
                **(details or {}),
            },
        )

    def _accumulate_budget(
        self, task_id: str, *, offloaded_chars: int = 0, clipped_chars: int = 0
    ) -> None:
        st = self._task_statuses.get(task_id)
        if not st:
            return
        if offloaded_chars > 0:
            st.offloaded_count += 1
            st.offloaded_chars += offloaded_chars
        if clipped_chars > 0:
            st.clipped_count += 1
            st.clipped_chars += clipped_chars
        st.updated_at = time.time()

    async def _push_milestone(
        self, task_id: str, label: str, iteration: int, max_iter: int, origin: dict[str, str]
    ) -> None:
        st = self._task_statuses.get(task_id)
        content = f"⏳ [{self._sanitize_visible(label)}] {iteration}/{max_iter}"
        if st:
            content += f", {st.tool_steps} tools"
            if st.phase.startswith("tool:"):
                content += f" — {st.phase}"
            # Append recent actions for transparency
            if st.recent_actions:
                recent = st.recent_actions[-3:]  # show last 3 actions in milestone
                content += "\n" + "\n".join(f"  → {self._sanitize_visible(a)}" for a in recent)
            if st.clipped_count or st.offloaded_count:
                content += (
                    f"\n  budget clip:{st.clipped_count}/{st.clipped_chars} "
                    f"offload:{st.offloaded_count}/{st.offloaded_chars}"
                )
        try:
            await self.bus.publish_outbound(
                OutboundMessage(
                    channel=origin["channel"],
                    chat_id=origin["chat_id"],
                    content=content,
                    metadata={
                        "_progress": True,
                        "_subagent_progress": True,
                        "task_id": task_id,
                        "_stream_event_schema": 1,
                        "_stream_event_type": "task_status",
                        "_stream_event_payload": {
                            "iteration": iteration,
                            "tool_steps": st.tool_steps if st else 0,
                            "status": st.status if st else "running",
                            "phase": st.phase if st else "starting",
                        },
                    },
                )
            )
        except Exception:
            logger.debug("Subagent [{}] milestone push failed (non-fatal)", task_id)

    def _cleanup_completed(self) -> None:
        finished = [(tid, st) for tid, st in self._task_statuses.items() if st.status != "running"]
        if len(finished) <= self._MAX_COMPLETED:
            return
        finished.sort(key=lambda x: x[1].updated_at)
        for tid, _ in finished[: len(finished) - self._MAX_COMPLETED]:
            self._task_statuses.pop(tid, None)

    @staticmethod
    def _strip_think(text: str | None) -> str | None:
        return shared.strip_think_tags(text)

    async def _call_experience_llm(self, system: str, prompt: str) -> dict[str, Any] | None:
        return await shared.call_experience_llm(
            system,
            prompt,
            experience_mode=self._experience_mode,
            provider=self.provider,
            model=self.model,
            utility_provider=self._utility_provider,
            utility_model=self._utility_model,
            service_tier=self.service_tier,
        )

    @staticmethod
    def _has_tool_error(tool_name: str, result: object) -> bool:
        return shared.has_tool_error(tool_name, result, _SUBAGENT_ERROR_KEYWORDS)

    @staticmethod
    def _parse_tool_error(tool_name: str, result: object):
        return shared.parse_tool_error(tool_name, result, _SUBAGENT_ERROR_KEYWORDS)

    async def _compress_state(
        self,
        tool_trace: list[str],
        reasoning_snippets: list[str],
        failed_directions: list[str],
        previous_state: str | None = None,
    ) -> str | None:
        return await shared.compress_state(
            tool_trace,
            reasoning_snippets,
            failed_directions,
            previous_state,
            experience_mode=self._experience_mode,
            llm_fn=self._call_experience_llm,
            label="subagent",
        )

    async def _check_sufficiency(
        self, user_request: str, tool_trace: list[str], last_state_text: str | None = None
    ) -> bool:
        return await shared.check_sufficiency(
            user_request,
            tool_trace,
            experience_mode=self._experience_mode,
            llm_fn=self._call_experience_llm,
            last_state_text=last_state_text,
        )

    def _compact_messages(
        self,
        messages: list[dict[str, Any]],
        initial_messages: list[dict[str, Any]],
        last_state_text: str | None,
        artifact_store: ArtifactStore | None,
    ) -> list[dict[str, Any]]:
        return shared.compact_messages(
            messages,
            initial_messages,
            last_state_text,
            artifact_store,
            keep_blocks=self._compact_keep_blocks,
            label="subagent",
        )

    @staticmethod
    def _budget_items(items: list[str], max_items: int, max_chars: int) -> list[str]:
        result: list[str] = []
        total = 0
        for item in items:
            if len(result) >= max_items:
                break
            remaining = max_chars - total
            if remaining <= 0:
                break
            piece = item if len(item) <= remaining else item[:remaining]
            result.append(piece)
            total += len(piece)
        return result

    async def _get_related_memory(self, task: str) -> tuple[list[str], list[str]]:
        if self._memory is None:
            return [], []
        bundle = await asyncio.to_thread(
            self._memory.recall,
            task,
            related_limit=3,
            experience_limit=3,
            include_long_term=False,
        )
        return list(bundle.related_memory), list(bundle.related_experience)

    def _setup_subagent_tools(
        self, task_id: str, origin: dict[str, str]
    ) -> tuple[ToolRegistry, Any, list[str], bool, bool]:
        tools = ToolRegistry()
        allowed_dir = self.workspace if self.restrict_to_workspace else None
        tools.register(ReadFileTool(workspace=self.workspace, allowed_dir=allowed_dir))
        tools.register(WriteFileTool(workspace=self.workspace, allowed_dir=allowed_dir))
        tools.register(EditFileTool(workspace=self.workspace, allowed_dir=allowed_dir))
        tools.register(ListDirTool(workspace=self.workspace, allowed_dir=allowed_dir))
        tools.register(
            RuntimeDiagnosticsTool(
                store=self._runtime_diagnostics,
                allowed_sources=("subagent",),
                pinned_session_key=task_id,
                allow_logs=False,
                allow_tool_observability=False,
            )
        )
        tools.register(
            ExecTool(
                working_dir=str(self.workspace),
                timeout=self.exec_config.timeout,
                restrict_to_workspace=self.restrict_to_workspace,
                path_append=self.exec_config.path_append,
                sandbox_mode=self.exec_config.sandbox_mode,
            )
        )

        channel = origin.get("channel", "gateway")
        chat_id = origin.get("chat_id", "direct")
        coding_tools: list[str] = []
        has_browser = False
        from bao.agent.tools.agent_browser import AgentBrowserTool
        from bao.agent.tools.coding_agent import CodingAgentDetailsTool, CodingAgentTool

        coding_tool = CodingAgentTool(workspace=self.workspace, allowed_dir=allowed_dir)
        if coding_tool.available_backends:
            coding_details = CodingAgentDetailsTool(parent=coding_tool)
            coding_tool.set_context(
                channel,
                chat_id,
                session_key=origin.get("session_key"),
            )
            coding_details.set_context(
                channel,
                chat_id,
                session_key=origin.get("session_key"),
            )
            tools.register(coding_tool)
            tools.register(coding_details)
            coding_tools.extend(coding_tool.available_backends)

        image_api_key = (
            self.image_generation_config.api_key.get_secret_value()
            if self.image_generation_config
            else ""
        )
        if self.image_generation_config and image_api_key:
            from bao.agent.tools.image_gen import ImageGenTool

            tools.register(
                ImageGenTool(
                    api_key=image_api_key,
                    model=self.image_generation_config.model,
                    base_url=self.image_generation_config.base_url,
                )
            )

        if self.desktop_config and self.desktop_config.enabled:
            try:
                from bao.agent.tools.desktop import (
                    ClickTool,
                    DragTool,
                    GetScreenInfoTool,
                    KeyPressTool,
                    ScreenshotTool,
                    ScrollTool,
                    TypeTextTool,
                )

                tools.register(ScreenshotTool())
                tools.register(ClickTool())
                tools.register(TypeTextTool())
                tools.register(KeyPressTool())
                tools.register(ScrollTool())
                tools.register(DragTool())
                tools.register(GetScreenInfoTool())
            except ImportError:
                pass

        search_tool = WebSearchTool(search_config=self.search_config, proxy=self.web_proxy)
        has_search = bool(search_tool.brave_key or search_tool.tavily_key or search_tool.exa_key)
        if has_search:
            tools.register(search_tool)
        web_fetch_tool = WebFetchTool(
            proxy=self.web_proxy,
            workspace=self.workspace,
            browser_enabled=self.browser_enabled,
            allowed_dir=allowed_dir,
        )
        web_fetch_tool.set_context(channel, chat_id, session_key=origin.get("session_key"))
        tools.register(web_fetch_tool)
        browser_tool = AgentBrowserTool(
            workspace=self.workspace,
            enabled=self.browser_enabled,
            allowed_dir=allowed_dir,
        )
        if self.browser_enabled:
            browser_tool.set_context(channel, chat_id, session_key=origin.get("session_key"))
            tools.register(browser_tool)
            has_browser = browser_tool.available

        return tools, coding_tool, coding_tools, has_search, has_browser

    def _maybe_cleanup_stale_artifacts(self) -> None:
        if not self._artifact_cleanup_done and self._ctx_mgmt in ("auto", "aggressive"):
            self._artifact_cleanup_done = True
            try:
                ArtifactStore(
                    self.workspace, "_stale_", self._artifact_retention_days
                ).cleanup_stale()
            except Exception as exc:
                logger.debug("subagent ctx stale cleanup failed: {}", exc)

    def _prepare_subagent_messages(
        self,
        task_id: str,
        task: str,
        *,
        child_session_key: str | None,
        channel: str | None,
        has_search: bool,
        has_browser: bool,
        coding_tools: list[str],
        related_memory: list[str],
        related_experience: list[str],
        context_from: str | None,
    ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
        system_prompt = self._build_subagent_prompt(
            task,
            channel=channel,
            has_search=has_search,
            has_browser=has_browser,
            coding_tools=coding_tools,
            related_memory=related_memory,
            related_experience=related_experience,
        )
        history = self._child_session_history(child_session_key) if child_session_key else []
        messages: list[dict[str, Any]] = [{"role": "system", "content": system_prompt}, *history]

        current = self.get_task_status(task_id)
        resume_ctx = current.resume_context if current else None
        if not resume_ctx and context_from:
            resume_ctx, warning = self._resolve_resume_context(context_from)
            if warning is not None:
                logger.debug("context_from={} not found or not finished, ignoring", context_from)
        if resume_ctx:
            messages.insert(1, {"role": "user", "content": resume_ctx})

        if not history:
            messages.append({"role": "user", "content": task})

        return messages, list(messages)

    def _create_subagent_artifact_store(self, task_id: str) -> ArtifactStore | None:
        if self._ctx_mgmt not in ("auto", "aggressive"):
            return None
        return ArtifactStore(self.workspace, f"subagent_{task_id}", self._artifact_retention_days)

    def _build_subagent_tool_exposure_snapshot(
        self,
        *,
        task: str,
        tools: ToolRegistry,
        force_final_response: bool,
    ) -> ToolExposureSnapshot:
        tool_definitions, slim_schema = tools.get_budgeted_definitions(
            names=None if not force_final_response else set()
        )
        ordered_tool_names = () if force_final_response else tuple(tools.tool_names)
        return ToolExposureSnapshot(
            mode="subagent",
            exposure_level=0,
            force_final_response=force_final_response,
            route_text=task,
            ordered_tool_names=ordered_tool_names,
            available_tool_lines=tuple(
                build_available_tool_lines(
                    registry=tools,
                    selected_tool_names=list(ordered_tool_names),
                )
            ),
            tool_definitions=tuple(tool_definitions),
            slim_schema=slim_schema,
        )

    def _archive_subagent_run_artifact(
        self,
        *,
        task_id: str,
        task: str,
        artifact_store: ArtifactStore | None,
        started_at: str,
        finished_at: str,
        state: RunLoopState,
        tool_trace: list[str],
        tools_used: list[str],
        reasoning_snippets: list[str],
        tool_exposure_history: list[ToolExposureSnapshot],
        provider_finish_reason: str,
        exit_reason: str,
    ) -> None:
        try:
            diagnostics_snapshot = self._runtime_diagnostics.snapshot(
                max_events=12,
                max_log_lines=0,
                allowed_sources=["subagent"],
                allowed_session_keys=[task_id],
            )
            run_artifact = build_run_artifact_payload(
                run_kind="subagent",
                session_key=task_id,
                model=self.model,
                started_at=started_at,
                finished_at=finished_at,
                user_request=task,
                tool_signal_text=None,
                final_content=state.final_content,
                exit_reason=exit_reason,
                provider_finish_reason=provider_finish_reason,
                provider_error=state.provider_error,
                interrupted=state.interrupted,
                total_errors=state.total_errors,
                tools_used=tools_used,
                tool_trace=tool_trace,
                reasoning_snippets=reasoning_snippets,
                last_state_text=state.last_state_text,
                tool_exposure_history=tool_exposure_history,
                tool_observability={},
                diagnostics_snapshot=diagnostics_snapshot,
            )
            store = artifact_store or ArtifactStore(
                self.workspace,
                f"subagent_{task_id}",
                self._artifact_retention_days,
            )
            _ = store.archive_json("trajectory", "subagent_run", run_artifact)
        except Exception as exc:
            logger.debug("subagent run artifact archive failed: {}", exc)

    async def _run_iteration_prechecks(
        self,
        *,
        task: str,
        messages: list[dict[str, Any]],
        initial_messages: list[dict[str, Any]],
        artifact_store: ArtifactStore | None,
        tool_trace: list[str],
        sufficiency_trace: list[str],
        reasoning_snippets: list[str],
        failed_directions: list[str],
        state: RunLoopState,
    ) -> list[dict[str, Any]]:
        return await apply_pre_iteration_checks(
            messages=messages,
            initial_messages=initial_messages,
            user_request=task,
            artifact_store=artifact_store,
            state=state,
            tool_trace=tool_trace,
            reasoning_snippets=reasoning_snippets,
            failed_directions=failed_directions,
            sufficiency_trace=sufficiency_trace,
            ctx_mgmt=self._ctx_mgmt,
            compact_bytes=self._compact_bytes,
            compress_state=self._compress_state,
            check_sufficiency=self._check_sufficiency,
            compact_messages=self._compact_messages,
        )

    async def _chat_subagent(
        self,
        messages: list[dict[str, Any]],
        tools: ToolRegistry,
        *,
        force_final_response: bool,
    ) -> Any:
        current_tools = [] if force_final_response else tools.get_budgeted_definitions()[0]
        return await shared.call_provider_chat(
            provider=self.provider,
            messages=messages,
            tools=current_tools,
            model=self.model,
            temperature=self.temperature,
            max_tokens=self.max_tokens,
            reasoning_effort=self.reasoning_effort,
            service_tier=self.service_tier,
            source="subagent",
            patched_log_label="Subagent repaired",
        )

    def _setup_coding_progress_callback(
        self,
        task_id: str,
        tool_call: Any,
        coding_tool: Any,
        *,
        tool_step: int,
    ) -> Any | None:
        progress_backend = None
        if tool_call.name == "coding_agent":
            backend_name = tool_call.arguments.get("agent")
            if isinstance(backend_name, str):
                backend = coding_tool._backends.get(backend_name)
                if backend and hasattr(backend, "set_progress_callback"):
                    step_index = tool_step + 1

                    async def _on_coding_progress(line: str) -> None:
                        progress = self._normalize_progress_line(line)
                        if not progress:
                            return
                        self._update_status(
                            task_id,
                            phase="tool:coding_agent",
                            tool_steps=step_index,
                            action=f"{backend_name}: {progress}",
                        )

                    backend.set_progress_callback(_on_coding_progress)
                    progress_backend = backend
        return progress_backend

    def _handle_screenshot_marker(self, tool_name: str, result: str) -> tuple[str, str | None]:
        result_text, screenshot_image_b64 = shared.handle_screenshot_marker(
            tool_name,
            result,
            read_error_label="子代截图失败 / screenshot read failed",
            unsafe_path_label="子代忽略非安全截图路径 / ignored unsafe screenshot path",
        )
        return str(result_text), screenshot_image_b64

    async def _execute_tool_call_block(
        self,
        *,
        task_id: str,
        tool_call: Any,
        tools: ToolRegistry,
        coding_tool: Any,
        artifact_store: ArtifactStore | None,
        messages: list[dict[str, Any]],
        tool_trace: list[str],
        sufficiency_trace: list[str],
        failed_directions: list[str],
        state: RunLoopState,
    ) -> None:
        action_preview = shared.summarize_tool_args_for_trace(
            tool_call.name,
            tool_call.arguments,
            max_len=50,
        )
        action_summary = f"{tool_call.name}({action_preview})"
        self._update_status(
            task_id,
            phase=f"tool:{tool_call.name}",
            tool_steps=state.total_tool_steps_for_sufficiency + 1,
            action=action_summary,
        )

        args_str = self._redact_tool_args_for_log(tool_call.name, tool_call.arguments)
        trace_args_preview = shared.summarize_tool_args_for_trace(
            tool_call.name,
            tool_call.arguments,
        )
        logger.debug(
            "Subagent [{}] executing: {} with arguments: {}",
            task_id,
            tool_call.name,
            args_str,
        )

        progress_backend = self._setup_coding_progress_callback(
            task_id,
            tool_call,
            coding_tool,
            tool_step=state.total_tool_steps_for_sufficiency,
        )
        execute_kwargs = shared.build_tool_execute_kwargs(
            tools.execute,
            raw_arguments=tool_call.raw_arguments,
            argument_parse_error=tool_call.argument_parse_error,
        )
        try:
            raw_result = await tools.execute(tool_call.name, tool_call.arguments, **execute_kwargs)
        finally:
            if progress_backend and hasattr(progress_backend, "set_progress_callback"):
                progress_backend.set_progress_callback(None)

        result, budget_event = apply_tool_output_budget(
            store=artifact_store,
            tool_name=tool_call.name,
            tool_call_id=tool_call.id,
            result=tool_result_payload(raw_result),
            offload_chars=self._tool_offload_chars,
            preview_chars=self._tool_preview_chars,
            hard_chars=self._tool_hard_chars,
            ctx_mgmt=self._ctx_mgmt,
        )
        self._accumulate_budget(
            task_id,
            offloaded_chars=budget_event.offloaded_chars,
            clipped_chars=budget_event.hard_clipped_chars,
        )

        result, screenshot_image_b64 = self._handle_screenshot_marker(tool_call.name, result)
        tool_msg: dict[str, Any] = {
            "role": "tool",
            "tool_call_id": tool_call.id,
            "name": tool_call.name,
            "content": result,
        }
        if screenshot_image_b64:
            tool_msg["_image"] = screenshot_image_b64
        messages.append(tool_msg)

        _tool_err_info = self._parse_tool_error(tool_call.name, raw_result)
        has_error = bool(_tool_err_info and _tool_err_info.is_error)
        trace_idx = len(tool_trace) + 1
        trace_entry = shared.build_tool_trace_entry(
            trace_idx,
            tool_call.name,
            trace_args_preview,
            has_error,
            result,
        )
        tool_trace.append(trace_entry)
        sufficiency_trace.append(trace_entry)
        if len(sufficiency_trace) > 32:
            del sufficiency_trace[:-32]
        state.total_tool_steps_for_sufficiency += 1

        if has_error:
            state.total_errors += 1
            state.consecutive_errors += 1
            failed_preview = shared.summarize_tool_args_for_trace(
                tool_call.name,
                tool_call.arguments,
                max_len=60,
            )
            shared.push_failed_direction(
                failed_directions,
                f"{tool_call.name}({failed_preview})",
            )
            if _tool_err_info:
                ts = self._task_statuses.get(task_id)
                task_label = ts.label if ts else ""
                if ts:
                    ts.last_error_category = _tool_err_info.category
                    ts.last_error_code = _tool_err_info.code
                    # sanitize message through existing clean helper
                    raw_msg = _tool_err_info.message or _tool_err_info.category
                    ts.last_error_message = self._sanitize_visible(raw_msg)
                self._record_runtime_diagnostic(
                    stage="tool_call",
                    message=_tool_err_info.message,
                    code=_tool_err_info.code or _tool_err_info.category,
                    retryable=_tool_err_info.retryable,
                    task_id=task_id,
                    label=task_label,
                    details={
                        "tool_name": tool_call.name,
                        "excerpt": _tool_err_info.raw_excerpt,
                    },
                )
        elif _tool_err_info and _tool_err_info.category == ToolErrorCategory.INTERRUPTED:
            state.consecutive_errors = 0
        else:
            state.consecutive_errors = 0

    async def _finalize_subagent_success(
        self,
        *,
        task_id: str,
        label: str,
        task: str,
        final_result: str | None,
        origin: dict[str, str],
        iteration: int,
        tool_step: int,
    ) -> None:
        if final_result is None:
            final_result = "Task completed but no final response was generated."

        self._update_status(
            task_id,
            phase="completed",
            status="completed",
            iteration=iteration,
            tool_steps=tool_step,
            result_summary=final_result[:500] if final_result else None,
        )
        child_session_key = origin.get("child_session_key")
        parent_session_key = origin.get("session_key")
        if child_session_key and parent_session_key:
            self._persist_child_result(
                child_session_key,
                parent_session_key=parent_session_key,
                label=label,
                task_id=task_id,
                result=final_result,
                status="completed",
            )

        logger.info("✅ 子代完成 / subagent done: [{}]", task_id)
        await self._announce_result_non_fatal(task_id, label, task, final_result, origin, "ok")

    async def _finalize_subagent_failure(
        self,
        *,
        task_id: str,
        label: str,
        task: str,
        origin: dict[str, str],
        error: Exception,
    ) -> None:
        error_msg = f"Error: {str(error)}"
        self._update_status(
            task_id,
            status="failed",
            phase="failed",
            result_summary=error_msg[:500],
        )
        child_session_key = origin.get("child_session_key")
        parent_session_key = origin.get("session_key")
        if child_session_key and parent_session_key:
            self._persist_child_result(
                child_session_key,
                parent_session_key=parent_session_key,
                label=label,
                task_id=task_id,
                result=error_msg,
                status="failed",
            )
        status = self._task_statuses.get(task_id)
        status_code = (
            status.last_error_code if status and status.last_error_code else "subagent_failed"
        )
        self._record_runtime_diagnostic(
            stage="failed",
            message=error_msg,
            code=status_code,
            retryable=False,
            task_id=task_id,
            label=label,
            details={
                "task": self._sanitize_visible(task[:200]),
                "last_error_category": status.last_error_category if status else None,
                "last_error_message": status.last_error_message if status else None,
            },
        )
        logger.error("❌ 子代失败 / subagent failed: [{}]: {}", task_id, error)
        await self._announce_result_non_fatal(task_id, label, task, error_msg, origin, "error")

    # ------------------------------------------------------------------
    # Internal: subagent execution
    # ------------------------------------------------------------------

    async def _run_subagent(
        self,
        task_id: str,
        task: str,
        label: str,
        origin: dict[str, str],
        context_from: str | None = None,
    ) -> None:
        logger.info("🚀 子代启动 / subagent start: [{}]: {}", task_id, label)
        started_at = datetime.now().isoformat(timespec="seconds")
        state = RunLoopState()
        artifact_store: ArtifactStore | None = None
        tool_trace: list[str] = []
        reasoning_snippets: list[str] = []
        sufficiency_trace: list[str] = []
        failed_directions: list[str] = []
        tool_exposure_history: list[ToolExposureSnapshot] = []
        tools_used: list[str] = []
        provider_finish_reason = ""
        exit_reason = "failed"
        try:
            child_session_key = origin.get("child_session_key")
            parent_session_key = origin.get("session_key")
            if child_session_key and parent_session_key:
                self._persist_child_user_turn(
                    child_session_key,
                    parent_session_key=parent_session_key,
                    label=label,
                    task_id=task_id,
                    task=task,
                )
            tools, coding_tool, coding_tools, has_search, has_browser = self._setup_subagent_tools(
                task_id, origin
            )
            related_memory, related_experience = await self._get_related_memory(task)
            self._maybe_cleanup_stale_artifacts()

            messages, initial_messages = self._prepare_subagent_messages(
                task_id,
                task,
                child_session_key=child_session_key,
                channel=origin.get("channel"),
                has_search=has_search,
                has_browser=has_browser,
                coding_tools=coding_tools,
                related_memory=related_memory,
                related_experience=related_experience,
                context_from=context_from,
            )
            artifact_store = self._create_subagent_artifact_store(task_id)

            max_iterations = self.max_iterations
            while state.iteration < max_iterations:
                state.iteration += 1
                messages = await self._run_iteration_prechecks(
                    task=task,
                    messages=messages,
                    initial_messages=initial_messages,
                    artifact_store=artifact_store,
                    tool_trace=tool_trace,
                    sufficiency_trace=sufficiency_trace,
                    reasoning_snippets=reasoning_snippets,
                    failed_directions=failed_directions,
                    state=state,
                )
                if state.interrupted:
                    exit_reason = "interrupted"
                    break

                self._update_status(task_id, iteration=state.iteration, phase="thinking")
                tool_exposure_history.append(
                    self._build_subagent_tool_exposure_snapshot(
                        task=task,
                        tools=tools,
                        force_final_response=state.force_final_response,
                    )
                )
                response = await self._chat_subagent(
                    messages,
                    tools,
                    force_final_response=state.force_final_response,
                )
                provider_finish_reason = str(response.finish_reason or "")
                if response.finish_reason == "interrupted":
                    state.interrupted = True
                    exit_reason = "interrupted"
                    break

                if response.has_tool_calls:
                    clean = self._strip_think(response.content)
                    if clean:
                        reasoning_snippets.append(clean[:200])

                    tool_call_dicts = [
                        tc.to_openai_tool_call()
                        for tc in response.tool_calls
                    ]
                    messages.append(
                        {
                            "role": "assistant",
                            "content": clean or "",
                            "tool_calls": tool_call_dicts,
                            "reasoning_content": response.reasoning_content,
                            "thinking_blocks": response.thinking_blocks,
                        }
                    )

                    for tool_call in response.tool_calls:
                        tools_used.append(tool_call.name)
                        await self._execute_tool_call_block(
                            task_id=task_id,
                            tool_call=tool_call,
                            tools=tools,
                            coding_tool=coding_tool,
                            artifact_store=artifact_store,
                            messages=messages,
                            tool_trace=tool_trace,
                            sufficiency_trace=sufficiency_trace,
                            failed_directions=failed_directions,
                            state=state,
                        )

                    error_feedback = build_error_feedback(
                        state.consecutive_errors,
                        failed_directions,
                    )
                    if error_feedback:
                        messages.append({"role": "user", "content": error_feedback})
                    elif (
                        state.total_tool_steps_for_sufficiency >= 8
                        and state.total_tool_steps_for_sufficiency % 4 == 0
                    ):
                        messages.append(
                            {
                                "role": "user",
                                "content": (
                                    f"[Progress: {state.total_tool_steps_for_sufficiency} steps completed]"
                                    " Focus on completing the task efficiently."
                                ),
                            }
                        )

                    if state.iteration % self._PROGRESS_INTERVAL == 0:
                        await self._push_milestone(
                            task_id,
                            label,
                            state.iteration,
                            max_iterations,
                            origin,
                        )
                    continue

                clean_final = self._strip_think(response.content)
                if response.finish_reason == "error":
                    state.provider_error = True
                    state.final_content = clean_final or "Error calling the AI model."
                    exit_reason = "provider_error"
                    break
                state.force_final_response, state.force_final_backoff_used, retry_prompt = (
                    shared.maybe_backoff_empty_final(
                        force_final_response=state.force_final_response,
                        force_final_backoff_used=state.force_final_backoff_used,
                        clean_final=clean_final,
                    )
                )
                if retry_prompt is not None:
                    messages.append(retry_prompt)
                    continue
                state.final_content = clean_final
                exit_reason = "completed" if clean_final is not None else "max_iterations"
                break

            if exit_reason == "provider_error":
                await self._finalize_subagent_failure(
                    task_id=task_id,
                    label=label,
                    task=task,
                    origin=origin,
                    error=RuntimeError(state.final_content or "Error calling the AI model."),
                )
            elif exit_reason == "interrupted":
                self._update_status(task_id, status="cancelled", phase="cancelled")
            else:
                await self._finalize_subagent_success(
                    task_id=task_id,
                    label=label,
                    task=task,
                    final_result=state.final_content,
                    origin=origin,
                    iteration=state.iteration,
                    tool_step=state.total_tool_steps_for_sufficiency,
                )

        except asyncio.CancelledError:
            state.interrupted = True
            exit_reason = "cancelled"
            self._update_status(task_id, status="cancelled", phase="cancelled")
            child_session_key = origin.get("child_session_key")
            parent_session_key = origin.get("session_key")
            if child_session_key and parent_session_key:
                self._persist_child_result(
                    child_session_key,
                    parent_session_key=parent_session_key,
                    label=label,
                    task_id=task_id,
                    result="Cancelled by user.",
                    status="cancelled",
                )
            logger.info("👋 子代终止 / subagent stopped: [{}]", task_id)

        except Exception as e:
            exit_reason = "failed"
            await self._finalize_subagent_failure(
                task_id=task_id,
                label=label,
                task=task,
                origin=origin,
                error=e,
            )
        finally:
            self._archive_subagent_run_artifact(
                task_id=task_id,
                task=task,
                artifact_store=artifact_store,
                started_at=started_at,
                finished_at=datetime.now().isoformat(timespec="seconds"),
                state=state,
                tool_trace=tool_trace,
                tools_used=tools_used,
                reasoning_snippets=reasoning_snippets,
                tool_exposure_history=tool_exposure_history,
                provider_finish_reason=provider_finish_reason,
                exit_reason=exit_reason,
            )

    async def _announce_result_non_fatal(
        self,
        task_id: str,
        label: str,
        task: str,
        result: str,
        origin: dict[str, str],
        status: str,
    ) -> None:
        try:
            await self._announce_result(task_id, label, task, result, origin, status)
        except asyncio.CancelledError:
            logger.debug("Subagent [{}] announce cancelled (non-fatal)", task_id)
        except Exception as exc:
            logger.debug("Subagent [{}] announce failed (non-fatal): {}", task_id, exc)

    async def _announce_result(
        self,
        task_id: str,
        label: str,
        task: str,
        result: str,
        origin: dict[str, str],
        status: str,
    ) -> None:
        safe_label = self._sanitize_visible(label)
        safe_task = self._sanitize_visible(task)
        safe_result = self._sanitize_visible(result)
        event = ControlEvent(
            kind=shared.SUBAGENT_RESULT_EVENT_TYPE,
            payload=shared.build_subagent_result_event(
                task_id=task_id,
                label=safe_label,
                task=safe_task,
                status=status,
                result=safe_result,
            ),
            session_key=origin.get("session_key", ""),
            origin_channel=origin["channel"],
            origin_chat_id=origin["chat_id"],
            source="subagent",
        )
        await self.bus.publish_control(event)
        logger.debug(
            "Subagent [{}] announced result to {}:{}", task_id, origin["channel"], origin["chat_id"]
        )

    def _build_subagent_prompt(
        self,
        task: str,
        *,
        channel: str | None,
        has_search: bool = False,
        has_browser: bool = False,
        coding_tools: list[str] | None = None,
        related_memory: list[str] | None = None,
        related_experience: list[str] | None = None,
    ) -> str:
        """Build a focused system prompt for the subagent."""
        from bao.agent.context import ContextBuilder, format_current_time

        search_capability = "\n- Search the web and fetch web pages" if has_search else ""
        browser_capability = (
            "\n- Control a browser for interactive pages, forms, screenshots, and DOM snapshots"
            if has_browser
            else ""
        )

        coding_capability = ""
        if coding_tools:
            names = ", ".join(coding_tools)
            coding_capability = (
                f"\n- coding_agent(agent=...): delegate coding to {names}.\n"
                "  PREFER coding_agent for multi-file changes, refactoring, debugging, "
                "and feature implementation over manual exec+read_file+write_file. "
                "Read the skill for usage: `bao/skills/coding-agent/SKILL.md` "
                "(or `skills/coding-agent/SKILL.md` if overridden in the workspace)."
            )
            if "opencode" in coding_tools:
                _omo_paths = [
                    self.workspace / ".opencode/oh-my-opencode.jsonc",
                    self.workspace / ".opencode/oh-my-opencode.json",
                    Path.home() / ".config/opencode/oh-my-opencode.jsonc",
                    Path.home() / ".config/opencode/oh-my-opencode.json",
                ]
                if any(p.exists() for p in _omo_paths):
                    coding_capability += (
                        "\n  OhMyOpenCode detected: use `ulw` prefix in opencode "
                        "prompts for enhanced orchestration mode."
                    )

        format_hint = ContextBuilder.get_channel_format_hint(channel)
        format_section = f"\n\n## Response Format\n{format_hint}" if format_hint else ""
        memory_section = ""
        if related_memory:
            budgeted_memory = self._budget_items(
                related_memory,
                max_items=self._memory_policy.related_memory_limit,
                max_chars=self._memory_policy.related_memory_chars,
            )
            if budgeted_memory:
                memory_section += "\n\n## Related Memory\n" + "\n---\n".join(budgeted_memory)
        if related_experience:
            budgeted_experience = self._budget_items(
                related_experience,
                max_items=self._memory_policy.related_experience_limit,
                max_chars=self._memory_policy.related_experience_chars,
            )
            if budgeted_experience:
                memory_section += (
                    "\n\n## Past Experience (lessons from similar tasks)\n"
                    + "\n---\n".join(budgeted_experience)
                )

        return f"""# Subagent

Current time: {format_current_time(include_weekday=False)}

You are a subagent spawned by the main agent to complete a specific task.

## Rules
1. Stay focused - complete only the assigned task, nothing else
2. Your final response will be reported back to the main agent
3. Do not initiate conversations or take on side tasks
4. Be concise but informative in your findings

## What You Can Do
- Read and write files in the workspace
- Execute shell commands
{search_capability}{browser_capability}{coding_capability}
- Complete the task thoroughly

## What You Cannot Do
- Send messages directly to users (no direct notify tool available)
- Spawn other subagents
- Access the main agent's conversation history
- Write or mutate memory/experience (read-only context only)

## Workspace
Your workspace is at: {self.workspace}
Skills may live in either of these locations:
- workspace overrides: {self.workspace}/skills/
- built-in skills: {Path(__file__).resolve().parent.parent / "skills"}
If the task matches a skill in those locations, read that `SKILL.md` before any substantive action.

When you have completed the task, provide a clear summary of your findings or actions.{memory_section}{format_section}"""
