"""
OpenCode Style Agent Factory

与 LangChain create_agent 接口完全兼容，但使用 OpenCode Style 的退出逻辑。

核心差异：
1. 退出条件：finish_reason not in ["tool-calls", "unknown"]（主动）
2. 退出位置：只在 model 节点退出
3. 不支持 return_direct（会被忽略）

参考设计文档：.claude/design/create-opencode-style-agent-design.md

OpenCode Style Agent 状态图（文字版，含 loop_controller）：

    START
      ↓
    （可选）before_agent 链：
        m1.before_agent → m2.before_agent → ... → loop_entry
      ↓
    （可选）before_model 链：
        m1.before_model → m2.before_model → ... → model
      ↓
    model 节点（同步/异步，带 OpenCode 退出判断）
      ↓
    （可选）after_model 链：
        ... → m2.after_model → m1.after_model
      ↓
    loop_exit_node（model 或最后一个 after_model）
      ↓
    loop_controller（集中处理 should_end / end_reason / 工具调用）：
      ├─ Case 1：should_end=True 且 end_reason == "max_steps_reached"
      │      → 在 messages 末尾追加 MAX_STEPS_PROMPT
      │      → 清空 should_end / end_reason，标记 force_text_exit=True
      │      → 跳回 loop_entry（before_model 或 model），进行最后一次「纯思考回答」
      │
      ├─ Case 2：should_end=True 且 end_reason != "max_steps_reached"
      │      → 直接结束：→ after_agent 链（如有）→ END
      │
      ├─ Case 3：should_end=False 且存在待执行工具调用
      │      → tools
      │         → prune_and_compress → loop_entry（对历史 Tool 输出做剪枝/压缩后再回到 model）
      │
      └─ Case 4：should_end=False 且无待执行工具调用（或有 structured_response）
             → 结束：→ after_agent 链（如有）→ END

    （可选）after_agent 链：
        ... → m2.after_agent → m1.after_agent
      ↓
     END
"""

from __future__ import annotations

import itertools
from dataclasses import dataclass, field
from typing import (
    TYPE_CHECKING,
    Annotated,
    Any,
    Generic,
    Literal,
    cast,
    get_args,
    get_origin,
    get_type_hints,
)

from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import AIMessage, AnyMessage, SystemMessage, ToolMessage
from langchain_core.tools import BaseTool
from langgraph._internal._runnable import RunnableCallable
from langgraph.constants import END, START
from langgraph.graph.state import StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt.tool_node import ToolCallWithContext, ToolNode
from langgraph.types import Command, Send
from typing_extensions import NotRequired, Required, TypedDict
import logging

from .exit_logic import (
    CONTINUE_FINISH_REASONS,
    OpenCodeStateFields,
    create_initial_state,
    determine_should_end,
    extract_finish_reason,
)
from .graph_edges import (
    _add_middleware_edge,
    _make_model_to_model_edge,
    _make_model_to_tools_edge,
    _make_tools_to_model_edge,
    _make_loop_controller_edge,
)
from .middleware_composition import (
    _chain_async_model_call_handlers,
    _chain_model_call_handlers,
    _chain_async_tool_call_wrappers,
    _chain_tool_call_wrappers,
    _get_can_jump_to,
)
from .schema_and_format import (
    STRUCTURED_OUTPUT_ERROR_TEMPLATE,
    FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT,
    _resolve_schema,
    _extract_metadata,
)
from .model_nodes import make_model_nodes
from .compaction import prune_and_compress_state
from .tool_call_degradation_corrector import (
    ToolCallDegradationCorrector,
    _build_allowed_tool_names_for_degradation_correction,
)

logger = logging.getLogger(__name__)

# ═══════════════════════════════════════════════════════════════════════════════
# LangChain 兼容性导入（支持新旧版本）
# ═══════════════════════════════════════════════════════════════════════════════
from langchain.agents.middleware.types import (
    AgentMiddleware,
    AgentState,
    ContextT,
    ExtendedModelResponse,
    JumpTo,
    ModelRequest,
    ModelResponse,
    OmitFromSchema,
    ResponseT,
    StateT_co,
    _InputAgentState,
    _OutputAgentState,
)

from langchain.agents.structured_output import (
    AutoStrategy,
    MultipleStructuredOutputsError,
    OutputToolBinding,
    ProviderStrategy,
    ProviderStrategyBinding,
    ResponseFormat,
    StructuredOutputError,
    StructuredOutputValidationError,
    ToolStrategy,
)
from langchain.chat_models import init_chat_model


if TYPE_CHECKING:
    from collections.abc import Awaitable, Callable, Sequence

    from langchain_core.runnables import Runnable, RunnableConfig
    from langgraph.cache.base import BaseCache
    from langgraph.graph.state import CompiledStateGraph
    from langgraph.runtime import Runtime
    from langgraph.store.base import BaseStore
    from langgraph.types import Checkpointer

    from langchain.agents.middleware.types import ToolCallRequest, ToolCallWrapper

DYNAMIC_TOOL_ERROR_TEMPLATE = """
Middleware added tools that the agent doesn't know how to execute.

Unknown tools: {unknown_tool_names}
Registered tools: {available_tool_names}

This happens when middleware modifies `request.tools` in `wrap_model_call` to include
tools that weren't passed to `create_agent()`.

How to fix this:

Option 1: Register tools at agent creation (recommended for most cases)
    Pass the tools to `create_agent(tools=[...])` or set them on `middleware.tools`.
    This makes tools available for every agent invocation.

Option 2: Handle dynamic tools in middleware (for tools created at runtime)
    Implement `wrap_tool_call` to execute tools that are added dynamically:

    class MyMiddleware(AgentMiddleware):
        def wrap_tool_call(self, request, handler):
            if request.tool_call["name"] == "dynamic_tool":
                # Execute the dynamic tool yourself or override with tool instance
                return handler(request.override(tool=my_dynamic_tool))
            return handler(request)
""".strip()


def _build_commands(
    model_response: ModelResponse,
    middleware_commands: list[Command[Any]] | None = None,
) -> list[Command[Any]]:
    """Build a list of Commands from a model response and middleware commands.

    The first Command contains the model response state (messages and optional
    structured_response). Middleware commands are appended as-is.

    Args:
        model_response: The model response containing messages and optional
            structured output.
        middleware_commands: Commands accumulated from middleware layers during
            composition (inner-first ordering).

    Returns:
        List of ``Command`` objects ready to be returned from a model node.
    """
    state: dict[str, Any] = {"messages": model_response.result}

    if model_response.structured_response is not None:
        state["structured_response"] = model_response.structured_response

    for cmd in middleware_commands or []:
        if cmd.goto:
            msg = (
                "Command goto is not yet supported in wrap_model_call middleware. "
                "Use the jump_to state field with before_model/after_model hooks instead."
            )
            raise NotImplementedError(msg)
        if cmd.resume:
            msg = "Command resume is not yet supported in wrap_model_call middleware."
            raise NotImplementedError(msg)
        if cmd.graph:
            msg = "Command graph is not yet supported in wrap_model_call middleware."
            raise NotImplementedError(msg)

    commands: list[Command[Any]] = [Command(update=state)]
    commands.extend(middleware_commands or [])
    return commands


def create_opencode_style_agent(
    model: str | BaseChatModel,
    tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,
    *,
    system_prompt: str | SystemMessage | None = None,
    middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),
    response_format: ResponseFormat[ResponseT] | type[ResponseT] | dict[str, Any] | None = None,
    state_schema: type[AgentState[ResponseT]] | None = None,
    context_schema: type[ContextT] | None = None,
    checkpointer: Checkpointer | None = None,
    store: BaseStore | None = None,
    interrupt_before: list[str] | None = None,
    interrupt_after: list[str] | None = None,
    debug: bool = False,
    name: str | None = None,
    cache: BaseCache[Any] | None = None,
    # OpenCode Style 特有参数
    max_steps: int = 50,
    enable_tool_call_degradation_fix: bool = True,
) -> CompiledStateGraph[
    AgentState[ResponseT], ContextT, _InputAgentState, _OutputAgentState[ResponseT]
]:
    """Creates an OpenCode style agent graph that uses finish_reason for exit control.

    与 LangChain create_agent 接口完全兼容，但使用 OpenCode Style 的退出逻辑。

    核心差异：
    1. 退出条件：finish_reason not in ["tool-calls", "unknown"]（LLM 主动决策）
    2. 退出位置：只在 model 节点退出
    3. 不支持 return_direct 工具属性（会被忽略）
    4. recursion_limit 初始值 = 2 * max_steps + 100

    Args:
        model: The language model for the agent.

            Can be a string identifier (e.g., `"openai:gpt-4"`) or a direct chat model
            instance (e.g., [`ChatOpenAI`][langchain_openai.ChatOpenAI] or other another
            [LangChain chat model](https://docs.langchain.com/oss/python/integrations/chat)).

            For a full list of supported model strings, see
            [`init_chat_model`][langchain.chat_models.init_chat_model(model_provider)].
        tools: A list of tools, `dict`, or `Callable`.

            If `None` or an empty list, the agent will consist of a model node without a
            tool calling loop.

            Note: `return_direct` tool attribute is ignored in OpenCode Style.
        system_prompt: An optional system prompt for the LLM.

            Can be a `str` (which will be converted to a `SystemMessage`) or a
            `SystemMessage` instance directly. The system message is added to the
            beginning of the message list when calling the model.
        middleware: A sequence of middleware instances to apply to the agent.

            Middleware can intercept and modify agent behavior at various stages.

            Note: Middleware can read OpenCode Style state fields (`should_end`,
            `finish_reason`, `end_reason`, `step`, `max_steps`, `finish_reasons`).
        response_format: An optional configuration for structured responses.

            Can be a `ToolStrategy`, `ProviderStrategy`, or a Pydantic model class.

            Note: In OpenCode Style, structured output tools do not trigger direct
            exit. LLM decides whether to end via `finish_reason="stop"`.
        state_schema: An optional `TypedDict` schema that extends `AgentState`.

            When provided, this schema is used instead of `AgentState` as the base
            schema for merging with middleware state schemas.

            Note: OpenCode Style adds fields: `step`, `max_steps`, `should_end`,
            `finish_reason`, `end_reason`, `finish_reasons`.
        context_schema: An optional schema for runtime context.
        checkpointer: An optional checkpoint saver object.

            Used for persisting the state of the graph (e.g., as chat memory) for a
            single thread (e.g., a single conversation).
        store: An optional store object.

            Used for persisting data across multiple threads (e.g., multiple
            conversations / users).
        interrupt_before: An optional list of node names to interrupt before.

            Useful if you want to add a user confirmation or other interrupt
            before taking an action.
        interrupt_after: An optional list of node names to interrupt after.

            Useful if you want to return directly or run additional processing
            on an output.
        debug: Whether to enable verbose logging for graph execution.

            When enabled, prints detailed information about each node execution, state
            updates, and transitions during agent runtime. Useful for debugging
            middleware behavior and understanding agent execution flow.
        name: An optional name for the `CompiledStateGraph`.

            This name will be automatically used when adding the agent graph to
            another graph as a subgraph node - particularly useful for building
            multi-agent systems.
        cache: An optional `BaseCache` instance to enable caching of graph execution.
        max_steps: Maximum thinking steps (OpenCode Style specific, default 50).

            When exceeded, the agent ends with `end_reason="max_steps_reached"`.
        enable_tool_call_degradation_fix: Whether to enable internal tool-call degradation
            correction wrapper on the underlying chat model. When enabled (default), the
            agent will attempt to recover structured ``tool_calls`` from degraded
            ``<tool_call>...</tool_call>`` text outputs.

    Returns:
        A compiled `StateGraph` that can be used for chat interactions.

    Raises:
        AssertionError: If duplicate middleware instances are provided.

    Note:
        - Exit is controlled by LLM's `finish_reason` (OpenCode Style)
        - `return_direct` tool attribute is ignored
        - `recursion_limit` initial value = 2 * max_steps + 100, can be overridden
          via `with_config()`

    Example:
        ```python
        from langchain_agentx.opencode_style_agent import create_opencode_style_agent


        def check_weather(location: str) -> str:
            '''Return the weather forecast for the specified location.'''
            return f"It's always sunny in {location}"


        graph = create_opencode_style_agent(
            model="anthropic:claude-sonnet-4-5-20250929",
            tools=[check_weather],
            system_prompt="You are a helpful assistant",
            max_steps=50,
        )
        inputs = {"messages": [{"role": "user", "content": "what is the weather in sf"}]}
        for chunk in graph.stream(inputs, stream_mode="updates"):
            print(chunk)
        ```
    """
    # init chat model
    if isinstance(model, str):
        model = init_chat_model(model)

    # Convert system_prompt to SystemMessage if needed
    system_message: SystemMessage | None = None
    if system_prompt is not None:
        if isinstance(system_prompt, SystemMessage):
            system_message = system_prompt
        else:
            system_message = SystemMessage(content=system_prompt)

    # Handle tools being None or empty
    if tools is None:
        tools = []

    # Convert response format and setup structured output tools
    # Raw schemas are wrapped in AutoStrategy to preserve auto-detection intent.
    # AutoStrategy is converted to ToolStrategy upfront to calculate tools during agent creation,
    # but may be replaced with ProviderStrategy later based on model capabilities.
    initial_response_format: ToolStrategy[Any] | ProviderStrategy[Any] | AutoStrategy[Any] | None
    if response_format is None:
        initial_response_format = None
    elif isinstance(response_format, (ToolStrategy, ProviderStrategy)):
        # Preserve explicitly requested strategies
        initial_response_format = response_format
    elif isinstance(response_format, AutoStrategy):
        # AutoStrategy provided - preserve it for later auto-detection
        initial_response_format = response_format
    else:
        # Raw schema - wrap in AutoStrategy to enable auto-detection
        initial_response_format = AutoStrategy(schema=response_format)

    # For AutoStrategy, convert to ToolStrategy to setup tools upfront
    # (may be replaced with ProviderStrategy later based on model)
    tool_strategy_for_setup: ToolStrategy[Any] | None = None
    if isinstance(initial_response_format, AutoStrategy):
        tool_strategy_for_setup = ToolStrategy(schema=initial_response_format.schema)
    elif isinstance(initial_response_format, ToolStrategy):
        tool_strategy_for_setup = initial_response_format

    structured_output_tools: dict[str, OutputToolBinding[Any]] = {}
    if tool_strategy_for_setup:
        for response_schema in tool_strategy_for_setup.schema_specs:
            structured_tool_info = OutputToolBinding.from_schema_spec(response_schema)
            structured_output_tools[structured_tool_info.tool.name] = structured_tool_info
    middleware_tools = [t for m in middleware for t in getattr(m, "tools", [])]

    # Collect middleware with wrap_tool_call or awrap_tool_call hooks
    # Include middleware with either implementation to ensure NotImplementedError is raised
    # when middleware doesn't support the execution path
    middleware_w_wrap_tool_call = [
        m
        for m in middleware
        if m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call
        or m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call
    ]

    # Chain all wrap_tool_call handlers into a single composed handler
    wrap_tool_call_wrapper = None
    if middleware_w_wrap_tool_call:
        wrappers = [m.wrap_tool_call for m in middleware_w_wrap_tool_call]
        wrap_tool_call_wrapper = _chain_tool_call_wrappers(wrappers)

    # Collect middleware with awrap_tool_call or wrap_tool_call hooks
    # Include middleware with either implementation to ensure NotImplementedError is raised
    # when middleware doesn't support the execution path
    middleware_w_awrap_tool_call = [
        m
        for m in middleware
        if m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call
        or m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call
    ]

    # Chain all awrap_tool_call handlers into a single composed async handler
    awrap_tool_call_wrapper = None
    if middleware_w_awrap_tool_call:
        async_wrappers = [m.awrap_tool_call for m in middleware_w_awrap_tool_call]
        awrap_tool_call_wrapper = _chain_async_tool_call_wrappers(async_wrappers)

    # Setup tools
    tool_node: ToolNode | None = None
    # Extract built-in provider tools (dict format) and regular tools (BaseTool/callables)
    built_in_tools = [t for t in tools if isinstance(t, dict)]
    regular_tools = [t for t in tools if not isinstance(t, dict)]

    # Tools that require client-side execution (must be in ToolNode)
    available_tools = middleware_tools + regular_tools

    # Create ToolNode if we have client-side tools OR if middleware defines wrap_tool_call
    # (which may handle dynamically registered tools)
    tool_node = (
        ToolNode(
            tools=available_tools,
            wrap_tool_call=wrap_tool_call_wrapper,
            awrap_tool_call=awrap_tool_call_wrapper,
        )
        if available_tools or wrap_tool_call_wrapper or awrap_tool_call_wrapper
        else None
    )

    # Default tools for ModelRequest initialization
    # Use converted BaseTool instances from ToolNode (not raw callables)
    # Include built-ins and converted tools (can be changed dynamically by middleware)
    # Structured tools are NOT included - they're added dynamically based on response_format
    if tool_node:
        default_tools = list(tool_node.tools_by_name.values()) + built_in_tools
    else:
        default_tools = list(built_in_tools)

    # Wrap underlying chat model with degradation corrector if enabled
    if enable_tool_call_degradation_fix and not isinstance(model, ToolCallDegradationCorrector):
        allowed_tool_names = _build_allowed_tool_names_for_degradation_correction(
            tool_node=tool_node,
            built_in_tools=built_in_tools,
            structured_output_tools=structured_output_tools,
        )
        model = ToolCallDegradationCorrector(
            llm=model,
            allowed_tool_names=allowed_tool_names,
        )

    # validate middleware
    if len({m.name for m in middleware}) != len(middleware):
        msg = "Please remove duplicate middleware instances."
        raise AssertionError(msg)
    middleware_w_before_agent = [
        m
        for m in middleware
        if m.__class__.before_agent is not AgentMiddleware.before_agent
        or m.__class__.abefore_agent is not AgentMiddleware.abefore_agent
    ]
    middleware_w_before_model = [
        m
        for m in middleware
        if m.__class__.before_model is not AgentMiddleware.before_model
        or m.__class__.abefore_model is not AgentMiddleware.abefore_model
    ]
    middleware_w_after_model = [
        m
        for m in middleware
        if m.__class__.after_model is not AgentMiddleware.after_model
        or m.__class__.aafter_model is not AgentMiddleware.aafter_model
    ]
    middleware_w_after_agent = [
        m
        for m in middleware
        if m.__class__.after_agent is not AgentMiddleware.after_agent
        or m.__class__.aafter_agent is not AgentMiddleware.aafter_agent
    ]
    # Collect middleware with wrap_model_call or awrap_model_call hooks
    # Include middleware with either implementation to ensure NotImplementedError is raised
    # when middleware doesn't support the execution path
    middleware_w_wrap_model_call = [
        m
        for m in middleware
        if m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call
        or m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call
    ]
    # Collect middleware with awrap_model_call or wrap_model_call hooks
    # Include middleware with either implementation to ensure NotImplementedError is raised
    # when middleware doesn't support the execution path
    middleware_w_awrap_model_call = [
        m
        for m in middleware
        if m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call
        or m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call
    ]

    # Compose wrap_model_call handlers into a single middleware stack (sync)
    wrap_model_call_handler = None
    if middleware_w_wrap_model_call:
        sync_handlers = [m.wrap_model_call for m in middleware_w_wrap_model_call]
        wrap_model_call_handler = _chain_model_call_handlers(sync_handlers)

    # Compose awrap_model_call handlers into a single middleware stack (async)
    awrap_model_call_handler = None
    if middleware_w_awrap_model_call:
        async_handlers = [m.awrap_model_call for m in middleware_w_awrap_model_call]
        awrap_model_call_handler = _chain_async_model_call_handlers(async_handlers)

    state_schemas: set[type] = {m.state_schema for m in middleware}
    # Use provided state_schema if available, otherwise use base AgentState
    base_state = state_schema if state_schema is not None else AgentState
    state_schemas.add(base_state)

    # ═══════════════════════════════════════════════════════════════════════
    # OpenCode Style: 添加 OpenCode 状态字段
    # ═══════════════════════════════════════════════════════════════════════
    class OpenCodeStateFields(TypedDict):
        """OpenCode Style 状态字段"""
        step: int  # 当前步骤数
        max_steps: int  # 最大步骤数
        should_end: bool  # 是否应该结束
        finish_reason: str | None  # 当前步骤的 finish_reason
        end_reason: str | None  # 最终结束原因
        finish_reasons: list[str]  # finish_reason 历史

    state_schemas.add(OpenCodeStateFields)

    resolved_state_schema = _resolve_schema(state_schemas, "StateSchema", None)
    input_schema = _resolve_schema(state_schemas, "InputSchema", "input")
    output_schema = _resolve_schema(state_schemas, "OutputSchema", "output")

    # create graph, add nodes
    graph: StateGraph[
        AgentState[ResponseT], ContextT, _InputAgentState, _OutputAgentState[ResponseT]
    ] = StateGraph(
        state_schema=resolved_state_schema,
        input_schema=input_schema,
        output_schema=output_schema,
        context_schema=context_schema,
    )

    # Use sync or async based on model capabilities
    model_node, amodel_node = make_model_nodes(
        model=model,
        default_tools=default_tools,
        system_message=system_message,
        initial_response_format=initial_response_format,
        max_steps=max_steps,
        name=name,
        structured_output_tools=structured_output_tools,
        tool_strategy_for_setup=tool_strategy_for_setup,
        wrap_model_call_handler=wrap_model_call_handler,
        awrap_model_call_handler=awrap_model_call_handler,
        tool_node=tool_node,
        wrap_tool_call_wrapper=wrap_tool_call_wrapper,
        awrap_tool_call_wrapper=awrap_tool_call_wrapper,
        dynamic_tool_error_template=DYNAMIC_TOOL_ERROR_TEMPLATE,
    )
    graph.add_node("model", RunnableCallable(model_node, amodel_node, trace=False))

    # Only add tools node if we have tools
    if tool_node is not None:
        graph.add_node("tools", tool_node)

        # OpenCode Style: 在 tools 和 loop_entry 之间插入主循环级上下文管理节点
        # 该节点调用 compaction.prune_and_compress_state，对历史 Tool 输出做剪枝 / 压缩，
        # 不修改退出语义（should_end / finish_reason 等）。
        def _prune_and_compress_node(state, runtime):
            # 传入当前模型实例，用于在 Compress 阶段调用 LLM 生成摘要
            return prune_and_compress_state(state, model=model)

        graph.add_node(
            "prune_and_compress",
            RunnableCallable(_prune_and_compress_node, None, trace=False),
        )

    # Add middleware nodes
    for m in middleware:
        if (
            m.__class__.before_agent is not AgentMiddleware.before_agent
            or m.__class__.abefore_agent is not AgentMiddleware.abefore_agent
        ):
            # Use RunnableCallable to support both sync and async
            # Pass None for sync if not overridden to avoid signature conflicts
            sync_before_agent = (
                m.before_agent
                if m.__class__.before_agent is not AgentMiddleware.before_agent
                else None
            )
            async_before_agent = (
                m.abefore_agent
                if m.__class__.abefore_agent is not AgentMiddleware.abefore_agent
                else None
            )
            before_agent_node = RunnableCallable(sync_before_agent, async_before_agent, trace=False)
            graph.add_node(
                f"{m.name}.before_agent", before_agent_node, input_schema=resolved_state_schema
            )

        if (
            m.__class__.before_model is not AgentMiddleware.before_model
            or m.__class__.abefore_model is not AgentMiddleware.abefore_model
        ):
            # Use RunnableCallable to support both sync and async
            # Pass None for sync if not overridden to avoid signature conflicts
            sync_before = (
                m.before_model
                if m.__class__.before_model is not AgentMiddleware.before_model
                else None
            )
            async_before = (
                m.abefore_model
                if m.__class__.abefore_model is not AgentMiddleware.abefore_model
                else None
            )
            before_node = RunnableCallable(sync_before, async_before, trace=False)
            graph.add_node(
                f"{m.name}.before_model", before_node, input_schema=resolved_state_schema
            )

        if (
            m.__class__.after_model is not AgentMiddleware.after_model
            or m.__class__.aafter_model is not AgentMiddleware.aafter_model
        ):
            # Use RunnableCallable to support both sync and async
            # Pass None for sync if not overridden to avoid signature conflicts
            sync_after = (
                m.after_model
                if m.__class__.after_model is not AgentMiddleware.after_model
                else None
            )
            async_after = (
                m.aafter_model
                if m.__class__.aafter_model is not AgentMiddleware.aafter_model
                else None
            )
            after_node = RunnableCallable(sync_after, async_after, trace=False)
            graph.add_node(f"{m.name}.after_model", after_node, input_schema=resolved_state_schema)

        if (
            m.__class__.after_agent is not AgentMiddleware.after_agent
            or m.__class__.aafter_agent is not AgentMiddleware.aafter_agent
        ):
            # Use RunnableCallable to support both sync and async
            # Pass None for sync if not overridden to avoid signature conflicts
            sync_after_agent = (
                m.after_agent
                if m.__class__.after_agent is not AgentMiddleware.after_agent
                else None
            )
            async_after_agent = (
                m.aafter_agent
                if m.__class__.aafter_agent is not AgentMiddleware.aafter_agent
                else None
            )
            after_agent_node = RunnableCallable(sync_after_agent, async_after_agent, trace=False)
            graph.add_node(
                f"{m.name}.after_agent", after_agent_node, input_schema=resolved_state_schema
            )

    # Determine the entry node (runs once at start): before_agent -> before_model -> model
    if middleware_w_before_agent:
        entry_node = f"{middleware_w_before_agent[0].name}.before_agent"
    elif middleware_w_before_model:
        entry_node = f"{middleware_w_before_model[0].name}.before_model"
    else:
        entry_node = "model"

    # Determine the loop entry node (beginning of agent loop, excludes before_agent)
    # This is where tools will loop back to for the next iteration
    if middleware_w_before_model:
        loop_entry_node = f"{middleware_w_before_model[0].name}.before_model"
    else:
        loop_entry_node = "model"

    # Determine the loop exit node (end of each iteration, can run multiple times)
    # This is after_model or model, but NOT after_agent
    if middleware_w_after_model:
        loop_exit_node = f"{middleware_w_after_model[0].name}.after_model"
    else:
        loop_exit_node = "model"

    # Determine the exit node (runs once at end): after_agent or END
    if middleware_w_after_agent:
        exit_node = f"{middleware_w_after_agent[-1].name}.after_agent"
    else:
        exit_node = END

    graph.add_edge(START, entry_node)

    # 添加 loop_controller 节点（自身不修改状态，只依赖条件边决策流向）
    graph.add_node("loop_controller", lambda state: state)

    # add conditional edges only if tools exist
    if tool_node is not None:
        # ═══════════════════════════════════════════════════════════════════════
        # OpenCode Style: tools 总是返回主循环入口。
        # 这里先经过 prune_and_compress 节点，对历史 Tool 输出做剪枝/占位，
        # 然后再回到 loop_entry_node（before_model 或 model）。
        # ═══════════════════════════════════════════════════════════════════════
        tools_to_model_destinations = ["prune_and_compress"]

        graph.add_conditional_edges(
            "tools",
            RunnableCallable(
                _make_tools_to_model_edge(
                    tool_node=tool_node,
                    model_destination="prune_and_compress",
                    structured_output_tools=structured_output_tools,
                    end_destination=exit_node,
                ),
                trace=False,
            ),
            tools_to_model_destinations,
        )

        # tools → prune_and_compress → loop_entry_node（before_model 或 model）
        graph.add_edge("prune_and_compress", loop_entry_node)

        # model/after_model 的 loop_exit_node 总是先进入 loop_controller，
        # 由 loop_controller 统一处理 should_end / end_reason / tool_calls 等逻辑
        graph.add_edge(loop_exit_node, "loop_controller")

        loop_controller_destinations = ["tools", loop_entry_node, exit_node]

        graph.add_conditional_edges(
            "loop_controller",
            RunnableCallable(
                _make_loop_controller_edge(
                    model_destination=loop_entry_node,
                    tools_destination="tools",
                    structured_output_tools=structured_output_tools,
                    end_destination=exit_node,
                ),
                trace=False,
            ),
            loop_controller_destinations,
        )
    elif len(structured_output_tools) > 0:
        graph.add_conditional_edges(
            loop_exit_node,
            RunnableCallable(
                _make_model_to_model_edge(
                    model_destination=loop_entry_node,
                    end_destination=exit_node,
                ),
                trace=False,
            ),
            [loop_entry_node, exit_node],
        )
    elif loop_exit_node == "model":
        # If no tools and no after_model, go directly to exit_node
        graph.add_edge(loop_exit_node, exit_node)
    # No tools but we have after_model - connect after_model to exit_node
    else:
        _add_middleware_edge(
            graph,
            name=f"{middleware_w_after_model[0].name}.after_model",
            default_destination=exit_node,
            model_destination=loop_entry_node,
            end_destination=exit_node,
            can_jump_to=_get_can_jump_to(middleware_w_after_model[0], "after_model"),
        )

    # Add before_agent middleware edges
    if middleware_w_before_agent:
        for m1, m2 in itertools.pairwise(middleware_w_before_agent):
            _add_middleware_edge(
                graph,
                name=f"{m1.name}.before_agent",
                default_destination=f"{m2.name}.before_agent",
                model_destination=loop_entry_node,
                end_destination=exit_node,
                can_jump_to=_get_can_jump_to(m1, "before_agent"),
            )
        # Connect last before_agent to loop_entry_node (before_model or model)
        _add_middleware_edge(
            graph,
            name=f"{middleware_w_before_agent[-1].name}.before_agent",
            default_destination=loop_entry_node,
            model_destination=loop_entry_node,
            end_destination=exit_node,
            can_jump_to=_get_can_jump_to(middleware_w_before_agent[-1], "before_agent"),
        )

    # Add before_model middleware edges
    if middleware_w_before_model:
        for m1, m2 in itertools.pairwise(middleware_w_before_model):
            _add_middleware_edge(
                graph,
                name=f"{m1.name}.before_model",
                default_destination=f"{m2.name}.before_model",
                model_destination=loop_entry_node,
                end_destination=exit_node,
                can_jump_to=_get_can_jump_to(m1, "before_model"),
            )
        # Go directly to model after the last before_model
        _add_middleware_edge(
            graph,
            name=f"{middleware_w_before_model[-1].name}.before_model",
            default_destination="model",
            model_destination=loop_entry_node,
            end_destination=exit_node,
            can_jump_to=_get_can_jump_to(middleware_w_before_model[-1], "before_model"),
        )

    # Add after_model middleware edges
    if middleware_w_after_model:
        graph.add_edge("model", f"{middleware_w_after_model[-1].name}.after_model")
        for idx in range(len(middleware_w_after_model) - 1, 0, -1):
            m1 = middleware_w_after_model[idx]
            m2 = middleware_w_after_model[idx - 1]
            _add_middleware_edge(
                graph,
                name=f"{m1.name}.after_model",
                default_destination=f"{m2.name}.after_model",
                model_destination=loop_entry_node,
                end_destination=exit_node,
                can_jump_to=_get_can_jump_to(m1, "after_model"),
            )
        # Note: Connection from after_model to after_agent/END is handled above
        # in the conditional edges section

    # Add after_agent middleware edges
    if middleware_w_after_agent:
        # Chain after_agent middleware (runs once at the very end, before END)
        for idx in range(len(middleware_w_after_agent) - 1, 0, -1):
            m1 = middleware_w_after_agent[idx]
            m2 = middleware_w_after_agent[idx - 1]
            _add_middleware_edge(
                graph,
                name=f"{m1.name}.after_agent",
                default_destination=f"{m2.name}.after_agent",
                model_destination=loop_entry_node,
                end_destination=exit_node,
                can_jump_to=_get_can_jump_to(m1, "after_agent"),
            )

        # Connect the last after_agent to END
        _add_middleware_edge(
            graph,
            name=f"{middleware_w_after_agent[0].name}.after_agent",
            default_destination=END,
            model_destination=loop_entry_node,
            end_destination=exit_node,
            can_jump_to=_get_can_jump_to(middleware_w_after_agent[0], "after_agent"),
        )

    # OpenCode Style: recursion_limit 基于 max_steps 计算
    # 公式：2 * max_steps + 100（每个 step 可能包含 model + tools 两个节点）
    config: RunnableConfig = {"recursion_limit": 2 * max_steps + 100}
    if name:
        config["metadata"] = {"lc_agent_name": name}

    return graph.compile(
        checkpointer=checkpointer,
        store=store,
        interrupt_before=interrupt_before,
        interrupt_after=interrupt_after,
        debug=debug,
        name=name,
        cache=cache,
    ).with_config(config)


# Backward compatibility alias
create_agent = create_opencode_style_agent


__all__ = [
    "create_opencode_style_agent",
    "create_agent",
]
