Ctrl K
ComunidadGuías…

butler/tools/delegate_subagent.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Delegate phase 2 — subagent loop construction (ENG-2)."""

from __future__ import annotations

from pathlib import Path
from typing import Any

from butler.tools.delegate_run_state import DelegateRunState
from butler.tools.delegate_subagent_ops import merge_agents_md_context_safe
from butler.delegate.subagent_permissions import (
    augment_subagent_tools_from_project,
    filter_tools_for_subagent,
)
from butler.tools.project_tools import get_tool_definitions_for_project
from butler.core.delegate_context import child_callbacks, get_parent_callbacks
from butler.tools.delegate_impl import _safe_dispatch
from butler.core.cache_safe_delegate import apply_cache_safe_system_prompt, delegate_diagnostics
from butler.core.delegate_context import get_parent_messages, get_parent_system_prompt
from butler.delegate.policy import resolve_delegate_max_iterations
from butler.tools.delegate_subagent_ops import apply_one_tool_per_iteration_safe
from butler.tools.delegate_subagent_ops import inject_dev_engine_prompt_safe
from butler.execution_context import get_current_session_key
from butler.tools.delegate_impl import _orchestrator_for_tool


def attach_agents_md_context(state: DelegateRunState) -> None:
    """Merge ``AGENTS.md`` into context for the current project (2b)."""

    merge_agents_md_context_safe(state)


def build_subagent_tools(state: DelegateRunState) -> None:
    """Populate ``state.tools`` and ``state.delegated_tools`` (2c base)."""

    state.tools = get_tool_definitions_for_project(state.project, role=state.role)
    workspace = Path(state.project.workspace) if state.project is not None else None
    state.delegated_tools = filter_tools_for_subagent(
        state.tools,
        workspace=workspace,
        role=state.role,
    )
    state.delegated_tools = augment_subagent_tools_from_project(
        state.delegated_tools,
        project=state.project,
        role=state.role,
    )


def apply_subagent_tool_filters(state: DelegateRunState) -> None:
    """Apply allow/deny allowlists from category_meta (2c allow/deny)."""
    allow_only = state.category_meta.get("allow_tools")
    deny_extra = state.category_meta.get("deny_tools")

    def _tool_name(t: dict[str, Any]) -> str:
        return str((t.get("function") or {}).get("name") or "")

    if isinstance(allow_only, list) and allow_only:
        allow_set = {str(t).strip() for t in allow_only if str(t).strip()}
        state.delegated_tools = [
            t for t in state.delegated_tools if _tool_name(t) in allow_set
        ]
    if isinstance(deny_extra, list):
        deny_set = {str(t).strip() for t in deny_extra if str(t).strip()}
        state.delegated_tools = [
            t for t in state.delegated_tools if _tool_name(t) not in deny_set
        ]


def create_project_agent_loop(state: DelegateRunState) -> None:
    """Create the child agent loop with safe dispatch + child callbacks (2d)."""

    parent_cb = get_parent_callbacks()
    state.agent = state.orch.create_project_agent_loop(
        role=state.role,
        tools=state.delegated_tools,
        tool_dispatcher=lambda name, args: _safe_dispatch(name, args, state.depth + 1),
        callbacks=child_callbacks(parent_cb),
        session_key=state.child_session_key or state.session_key,
    )


def apply_cache_safe_prompt(state: DelegateRunState) -> None:
    """Merge parent system prompt + record cache diagnostics (2e)."""

    parent_sys = get_parent_system_prompt()
    if not parent_sys:
        return
    parent_msgs = get_parent_messages()
    merged = apply_cache_safe_system_prompt(
        parent_sys,
        state.agent.system_prompt,
        tools=state.delegated_tools,
        messages=parent_msgs,
    )
    state.agent.system_prompt = merged
    state.agent.diagnostics.update(
        delegate_diagnostics(
            parent_sys,
            merged,
            tools=state.delegated_tools,
            messages=parent_msgs,
        )
    )


def configure_subagent_policy(state: DelegateRunState) -> None:
    """Apply max-iterations + one-tool-per-iter policy (2f)."""

    state.agent.config.max_iterations = resolve_delegate_max_iterations(state.category_meta)

    apply_one_tool_per_iteration_safe(state)


def inject_dev_engine_prompt(state: DelegateRunState) -> None:
    """Append dev engine system prompt when role=dev and engine enabled (2g)."""
    norm = state.role.replace("_agent", "").strip().lower()
    if norm != "dev":
        return

    inject_dev_engine_prompt_safe(state)


def inject_workspace_root_context(state: DelegateRunState) -> None:
    """Prepend deterministic workspace-relative path rules for sub-agents."""
    if state.project is None:
        return
    try:
        ws = Path(state.project.workspace).resolve()
    except (TypeError, ValueError, OSError):
        return
    if not ws.is_dir():
        return
    marker = "## 工作区(必守)"
    if marker in str(state.context or ""):
        return
    dir_name = ws.name
    block = (
        f"{marker}\n"
        f"- 项目 workspace 根目录:`{ws}`\n"
        "- 所有 read_file / write_file / patch 路径必须**相对此根**(例:`docs/foo.md`)\n"
        f"- **禁止** `{dir_name}/docs/...`、项目目录名前缀、或 workspace 外的绝对路径\n"
    )
    state.context = f"{block}\n{state.context}".strip() if state.context else block


def resolve_subagent(state: DelegateRunState) -> None:
    """Phase 2: build the project agent loop with cache-safe prompt."""

    state.orch = _orchestrator_for_tool(channel="cli")
    parent_sk = str(get_current_session_key() or "").strip()
    state.project = state.orch.project_manager.get_current(session_key=parent_sk)
    inject_workspace_root_context(state)
    attach_agents_md_context(state)
    build_subagent_tools(state)
    apply_subagent_tool_filters(state)
    create_project_agent_loop(state)
    inject_dev_engine_prompt(state)
    apply_cache_safe_prompt(state)
    configure_subagent_policy(state)
    state.agent.reset()


__all__ = [
    "apply_cache_safe_prompt",
    "apply_subagent_tool_filters",
    "attach_agents_md_context",
    "build_subagent_tools",
    "configure_subagent_policy",
    "create_project_agent_loop",
    "inject_dev_engine_prompt",
    "inject_workspace_root_context",
    "resolve_subagent",
]