import time
import uuid
from datetime import datetime, timezone
from unittest.mock import patch

import pytest

# Import shared fixtures and constants from conftest
from conftest import (
    CREATE_DELAY_SQLITE,
    DEFAULT_EMBEDDING_CONFIG,
    USING_SQLITE,
)
from sqlalchemy import func, select

from letta.constants import (
    LOCAL_ONLY_MULTI_AGENT_TOOLS,
    MULTI_AGENT_TOOLS,
)
from letta.errors import LettaAgentNotFoundError
from letta.orm.file import FileContent as FileContentModel
from letta.schemas.agent import CreateAgent, InternalTemplateAgentCreate, UpdateAgent
from letta.schemas.block import CreateBlock
from letta.schemas.embedding_config import EmbeddingConfig
from letta.schemas.enums import (
    MessageRole,
)
from letta.schemas.letta_message_content import TextContent
from letta.schemas.letta_stop_reason import StopReasonType
from letta.schemas.llm_config import LLMConfig
from letta.schemas.message import MessageCreate
from letta.schemas.source import Source as PydanticSource
from letta.schemas.tool_rule import InitToolRule
from letta.server.db import db_registry
from letta.server.server import SyncServer
from letta.services.helpers.agent_manager_helper import calculate_base_tools, calculate_multi_agent_tools, validate_agent_exists_async
from letta.services.summarizer.summarizer_config import CompactionSettings
from letta.settings import settings
from letta.utils import calculate_file_defaults_based_on_context_window
from tests.helpers.utils import comprehensive_agent_checks, validate_context_window_overview

# ======================================================================================================================
# Helper Functions
# ======================================================================================================================


async def _count_file_content_rows(session, file_id: str) -> int:
    q = select(func.count()).select_from(FileContentModel).where(FileContentModel.file_id == file_id)
    result = await session.execute(q)
    return result.scalar_one()


# ======================================================================================================================
# AgentManager Tests - Basic
# ======================================================================================================================
async def test_validate_agent_exists_async(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    """Test the validate_agent_exists_async helper function"""
    created_agent, _ = comprehensive_test_agent_fixture

    # test with valid agent
    async with db_registry.async_session() as session:
        # should not raise exception
        await validate_agent_exists_async(session, created_agent.id, default_user)

    # test with non-existent agent
    async with db_registry.async_session() as session:
        with pytest.raises(LettaAgentNotFoundError):
            await validate_agent_exists_async(session, "non-existent-id", default_user)


@pytest.mark.asyncio
async def test_create_get_list_agent(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    # Test agent creation
    created_agent, create_agent_request = comprehensive_test_agent_fixture
    comprehensive_agent_checks(created_agent, create_agent_request, actor=default_user)

    # Test get agent
    get_agent = await server.agent_manager.get_agent_by_id_async(agent_id=created_agent.id, actor=default_user)
    comprehensive_agent_checks(get_agent, create_agent_request, actor=default_user)

    # Test get agent name
    agents = await server.agent_manager.list_agents_async(name=created_agent.name, actor=default_user)
    get_agent_name = agents[0]
    comprehensive_agent_checks(get_agent_name, create_agent_request, actor=default_user)

    # Test list agent
    list_agents = await server.agent_manager.list_agents_async(actor=default_user)
    assert len(list_agents) == 1
    comprehensive_agent_checks(list_agents[0], create_agent_request, actor=default_user)

    # Test deleting the agent
    await server.agent_manager.delete_agent_async(get_agent.id, default_user)
    list_agents = await server.agent_manager.list_agents_async(actor=default_user)
    assert len(list_agents) == 0


@pytest.mark.asyncio
async def test_create_agent_include_base_tools(server: SyncServer, default_user):
    """Test agent creation with include_default_source=True"""
    # Upsert base tools
    await server.tool_manager.upsert_base_tools_async(actor=default_user)

    memory_blocks = [CreateBlock(label="human", value="TestUser"), CreateBlock(label="persona", value="I am a test assistant")]

    create_agent_request = CreateAgent(
        name="test_default_source_agent",
        agent_type="memgpt_v2_agent",
        system="test system",
        memory_blocks=memory_blocks,
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        include_base_tools=True,
    )

    # Create the agent
    created_agent = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )

    # Assert the tools exist
    tool_names = [t.name for t in created_agent.tools]
    expected_tools = calculate_base_tools(is_v2=True)
    assert sorted(tool_names) == sorted(expected_tools)


@pytest.mark.asyncio
async def test_create_agent_base_tool_rules_excluded_providers(server: SyncServer, default_user):
    """Test that include_base_tool_rules is overridden to False for excluded providers"""
    # Upsert base tools
    await server.tool_manager.upsert_base_tools_async(actor=default_user)

    memory_blocks = [CreateBlock(label="human", value="TestUser"), CreateBlock(label="persona", value="I am a test assistant")]

    # Test with excluded provider (openai)
    create_agent_request = CreateAgent(
        name="test_excluded_provider_agent",
        agent_type="memgpt_v2_agent",
        system="test system",
        memory_blocks=memory_blocks,
        llm_config=LLMConfig.default_config("gpt-4o-mini"),  # This has model_endpoint_type="openai"
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        include_base_tool_rules=False,
    )

    # Create the agent
    created_agent = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )

    # Assert that no base tool rules were added (since include_base_tool_rules was overridden to False)
    print(created_agent.tool_rules)
    assert created_agent.tool_rules is None or len(created_agent.tool_rules) == 0


@pytest.mark.asyncio
async def test_create_agent_base_tool_rules_non_excluded_providers(server: SyncServer, default_user):
    """Test that include_base_tool_rules is NOT overridden for non-excluded providers"""
    # Upsert base tools
    await server.tool_manager.upsert_base_tools_async(actor=default_user)

    memory_blocks = [CreateBlock(label="human", value="TestUser"), CreateBlock(label="persona", value="I am a test assistant")]

    # Test with non-excluded provider (together)
    create_agent_request = CreateAgent(
        name="test_non_excluded_provider_agent",
        agent_type="memgpt_v2_agent",
        system="test system",
        memory_blocks=memory_blocks,
        llm_config=LLMConfig(
            model="llama-3.1-8b-instruct",
            model_endpoint_type="together",  # Model doesn't match EXCLUDE_MODEL_KEYWORDS_FROM_BASE_TOOL_RULES
            model_endpoint="https://api.together.xyz",
            context_window=8192,
        ),
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        include_base_tool_rules=True,  # Should remain True
    )

    # Create the agent
    created_agent = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )

    # Assert that base tool rules were added (since include_base_tool_rules remained True)
    assert created_agent.tool_rules is not None
    assert len(created_agent.tool_rules) > 0


@pytest.mark.asyncio
async def test_create_agent_with_model_handle_uses_correct_llm_config(server: SyncServer, default_user):
    """When CreateAgent.model is provided, ensure the correct handle is used to resolve llm_config.

    This verifies that the model handle passed by the client is forwarded into
    SyncServer.get_llm_config_from_handle_async and that the resulting AgentState
    carries an llm_config with the same handle.
    """

    # Track the arguments used to resolve the LLM config
    captured_kwargs: dict = {}

    async def fake_get_llm_config_from_handle_async(self, actor, **kwargs):  # type: ignore[override]
        from letta.schemas.llm_config import LLMConfig as PydanticLLMConfig

        captured_kwargs.update(kwargs)
        handle = kwargs["handle"]

        # Return a minimal but valid LLMConfig with the requested handle
        return PydanticLLMConfig(
            model="test-model-name",
            model_endpoint_type="openai",
            model_endpoint="https://api.openai.com/v1",
            context_window=8192,
            handle=handle,
        )

    model_handle = "openai/gpt-4o-mini"

    # Patch SyncServer.get_llm_config_from_handle_async so we don't depend on provider DB state
    with patch.object(SyncServer, "get_llm_config_from_handle_async", new=fake_get_llm_config_from_handle_async):
        created_agent = await server.create_agent_async(
            request=CreateAgent(
                name="agent_with_model_handle",
                agent_type="memgpt_v2_agent",
                # Use new model handle field instead of llm_config
                model=model_handle,
                embedding_config=EmbeddingConfig.default_config(provider="openai"),
                memory_blocks=[],
                include_base_tools=False,
            ),
            actor=default_user,
        )

    # Ensure we resolved the config using the provided handle
    assert captured_kwargs["handle"] == model_handle

    # And that the resulting agent's llm_config reflects the same handle
    assert created_agent.llm_config is not None
    assert created_agent.llm_config.handle == model_handle


@pytest.mark.asyncio
async def test_compaction_settings_model_uses_separate_llm_config_for_summarization(server: SyncServer, default_user):
    """When compaction_settings.model differs from the agent model, use a separate llm_config.

    This test exercises the summarization helpers directly to avoid external
    provider dependencies. It verifies that CompactionSettings.model controls
    the LLMConfig used for the summarizer request.
    """

    from letta.schemas.agent import AgentState as PydanticAgentState
    from letta.schemas.enums import AgentType, MessageRole
    from letta.schemas.memory import Memory
    from letta.schemas.message import Message as PydanticMessage
    from letta.schemas.model import OpenAIModelSettings, OpenAIReasoning
    from letta.services.summarizer.compact import build_summarizer_llm_config

    await server.init_async(init_with_default_org_and_user=True)

    # Base agent LLM config
    base_llm_config = LLMConfig.default_config("gpt-4o-mini")
    assert base_llm_config.model == "gpt-4o-mini"

    # Configure compaction to use a different summarizer model (!= default openai summarizer model)
    summarizer_handle = "openai/gpt-5-nano"
    summarizer_model_settings = OpenAIModelSettings(
        max_output_tokens=1234,
        temperature=0.1,
        reasoning=OpenAIReasoning(reasoning_effort="high"),
        response_format=None,
    )
    summarizer_config = CompactionSettings(
        model=summarizer_handle,
        model_settings=summarizer_model_settings,
        prompt="You are a summarizer.",
        clip_chars=2000,
        mode="all",
        sliding_window_percentage=0.3,
    )

    # Minimal message buffer: system + one user + one assistant
    [
        PydanticMessage(
            role=MessageRole.system,
            content=[TextContent(type="text", text="You are a helpful assistant.")],
        ),
        PydanticMessage(
            role=MessageRole.user,
            content=[TextContent(type="text", text="Hello")],
        ),
        PydanticMessage(
            role=MessageRole.assistant,
            content=[TextContent(type="text", text="Hi there")],
        ),
    ]

    # Build a minimal AgentState for LettaAgentV3 using the base llm_config
    agent_state = PydanticAgentState(
        id="agent-test-compaction-llm-config",
        name="test-agent",
        system="You are a helpful assistant.",
        agent_type=AgentType.letta_v1_agent,
        llm_config=base_llm_config,
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        model=None,
        embedding=None,
        model_settings=None,
        compaction_settings=summarizer_config,
        response_format=None,
        description=None,
        metadata=None,
        memory=Memory(blocks=[]),
        blocks=[],
        tools=[],
        sources=[],
        tags=[],
        tool_exec_environment_variables=[],
        secrets=[],
        project_id=None,
        template_id=None,
        base_template_id=None,
        deployment_id=None,
        entity_id=None,
        identity_ids=[],
        identities=[],
        message_ids=[],
        message_buffer_autoclear=False,
        enable_sleeptime=None,
        multi_agent_group=None,
        managed_group=None,
        last_run_completion=None,
        last_run_duration_ms=None,
        last_stop_reason=None,
        timezone="UTC",
        max_files_open=None,
        per_file_view_window_char_limit=None,
        hidden=None,
        created_by_id=None,
        last_updated_by_id=None,
        created_at=None,
        updated_at=None,
        tool_rules=None,
    )

    # Use the shared function to derive summarizer llm_config
    summarizer_llm_config = await build_summarizer_llm_config(
        agent_llm_config=agent_state.llm_config,
        summarizer_config=agent_state.compaction_settings,
        actor=default_user,
    )

    # Agent model remains the base model
    assert agent_state.llm_config.model == "gpt-4o-mini"

    # Summarizer config should use the handle/model from compaction_settings
    assert summarizer_llm_config.handle == summarizer_handle
    assert summarizer_llm_config.model == "gpt-5-nano"
    # And should reflect overrides from model_settings
    assert summarizer_llm_config.max_tokens == 1234
    assert summarizer_llm_config.temperature == 0.1


@pytest.mark.asyncio
async def test_create_agent_sets_default_compaction_model_anthropic(server: SyncServer, default_user):
    """When no compaction_settings provided for Anthropic agent, default haiku model should be set."""
    from letta.schemas.agent import CreateAgent
    from letta.schemas.enums import ProviderType
    from letta.services.summarizer.summarizer_config import get_default_summarizer_model

    await server.init_async(init_with_default_org_and_user=True)

    # Upsert base tools
    await server.tool_manager.upsert_base_tools_async(actor=default_user)

    # Create agent without compaction_settings using Anthropic LLM
    agent = await server.create_agent_async(
        CreateAgent(
            name="test-default-compaction-anthropic",
            model="anthropic/claude-sonnet-4-5-20250929",
            # No compaction_settings
        ),
        actor=default_user,
    )

    # Should have default haiku model set
    assert agent.compaction_settings is not None
    assert agent.compaction_settings.model == get_default_summarizer_model(ProviderType.anthropic)


@pytest.mark.asyncio
async def test_create_agent_sets_default_compaction_model_openai(server: SyncServer, default_user):
    """When no compaction_settings provided for OpenAI agent, default gpt-5-mini model should be set."""
    from letta.schemas.agent import CreateAgent

    await server.init_async(init_with_default_org_and_user=True)

    # Upsert base tools
    await server.tool_manager.upsert_base_tools_async(actor=default_user)

    # Create agent without compaction_settings using OpenAI LLM
    agent = await server.create_agent_async(
        CreateAgent(
            name="test-default-compaction-openai",
            model="openai/gpt-4o-mini",
            # No compaction_settings
        ),
        actor=default_user,
    )

    # Should have default gpt-5-mini model set
    assert agent.compaction_settings is not None
    assert agent.compaction_settings.model == "openai/gpt-5-mini"


@pytest.mark.asyncio
async def test_create_agent_preserves_compaction_settings_when_model_set(server: SyncServer, default_user):
    """When compaction_settings.model is already set, it should not be overwritten."""
    from letta.schemas.agent import CreateAgent
    from letta.schemas.model import OpenAIModelSettings, OpenAIReasoning
    from letta.services.summarizer.summarizer_config import CompactionSettings

    await server.init_async(init_with_default_org_and_user=True)

    # Upsert base tools
    await server.tool_manager.upsert_base_tools_async(actor=default_user)

    summarizer_handle = "gpt-4o-mini"

    summarizer_config = CompactionSettings(
        model=summarizer_handle,
        model_settings=OpenAIModelSettings(max_output_tokens=1234, temperature=0.1, reasoning=OpenAIReasoning(reasoning_effort="high")),
        prompt="You are a summarizer.",
        clip_chars=2000,
        mode="all",
        sliding_window_percentage=0.3,
    )

    # Create agent with explicit compaction_settings model
    agent = await server.create_agent_async(
        CreateAgent(
            name="test-preserve-compaction",
            model="openai/gpt-5.2-codex",
            compaction_settings=summarizer_config,
        ),
        actor=default_user,
    )

    # Should preserve the custom model, not override with gpt-5-mini default
    assert agent.compaction_settings is not None
    assert agent.compaction_settings.model == summarizer_handle
    assert agent.compaction_settings.mode == "all"


@pytest.mark.asyncio
async def test_calculate_multi_agent_tools(set_letta_environment):
    """Test that calculate_multi_agent_tools excludes local-only tools in production."""
    result = calculate_multi_agent_tools()

    if settings.environment == "prod":
        # Production environment should exclude local-only tools
        expected_tools = set(MULTI_AGENT_TOOLS) - set(LOCAL_ONLY_MULTI_AGENT_TOOLS)
        assert result == expected_tools, "Production should exclude local-only multi-agent tools"
        assert not set(LOCAL_ONLY_MULTI_AGENT_TOOLS).intersection(result), "Production should not include local-only tools"

        # Verify specific tools
        assert "send_message_to_agent_and_wait_for_reply" in result, "Standard multi-agent tools should be in production"
        assert "send_message_to_agents_matching_tags" in result, "Standard multi-agent tools should be in production"
        assert "send_message_to_agent_async" not in result, "Local-only tools should not be in production"
    else:
        # Non-production environment should include all multi-agent tools
        assert result == set(MULTI_AGENT_TOOLS), "Non-production should include all multi-agent tools"
        assert set(LOCAL_ONLY_MULTI_AGENT_TOOLS).issubset(result), "Non-production should include local-only tools"

        # Verify specific tools
        assert "send_message_to_agent_and_wait_for_reply" in result, "All multi-agent tools should be in non-production"
        assert "send_message_to_agents_matching_tags" in result, "All multi-agent tools should be in non-production"
        assert "send_message_to_agent_async" in result, "Local-only tools should be in non-production"


async def test_upsert_base_tools_excludes_local_only_in_production(server: SyncServer, default_user, set_letta_environment):
    """Test that upsert_base_tools excludes local-only multi-agent tools in production."""
    # Upsert all base tools
    tools = await server.tool_manager.upsert_base_tools_async(actor=default_user)
    tool_names = {tool.name for tool in tools}

    if settings.environment == "prod":
        # Production environment should exclude local-only multi-agent tools
        for local_only_tool in LOCAL_ONLY_MULTI_AGENT_TOOLS:
            assert local_only_tool not in tool_names, f"Local-only tool '{local_only_tool}' should not be upserted in production"

        # But should include standard multi-agent tools
        standard_multi_agent_tools = set(MULTI_AGENT_TOOLS) - set(LOCAL_ONLY_MULTI_AGENT_TOOLS)
        for standard_tool in standard_multi_agent_tools:
            assert standard_tool in tool_names, f"Standard multi-agent tool '{standard_tool}' should be upserted in production"
    else:
        # Non-production environment should include all multi-agent tools
        for tool in MULTI_AGENT_TOOLS:
            assert tool in tool_names, f"Multi-agent tool '{tool}' should be upserted in non-production"


async def test_upsert_multi_agent_tools_only(server: SyncServer, default_user, set_letta_environment):
    """Test that upserting only multi-agent tools respects production filtering."""
    from letta.schemas.enums import ToolType

    # Upsert only multi-agent tools
    tools = await server.tool_manager.upsert_base_tools_async(actor=default_user, allowed_types={ToolType.LETTA_MULTI_AGENT_CORE})
    tool_names = {tool.name for tool in tools}

    if settings.environment == "prod":
        # Should only have non-local multi-agent tools
        expected_tools = set(MULTI_AGENT_TOOLS) - set(LOCAL_ONLY_MULTI_AGENT_TOOLS)
        assert tool_names == expected_tools, "Production multi-agent upsert should exclude local-only tools"
        assert "send_message_to_agent_async" not in tool_names, "Local-only async tool should not be upserted in production"
    else:
        # Should have all multi-agent tools
        assert tool_names == set(MULTI_AGENT_TOOLS), "Non-production multi-agent upsert should include all tools"
        assert "send_message_to_agent_async" in tool_names, "Local-only async tool should be upserted in non-production"


@pytest.mark.asyncio
async def test_create_agent_with_default_source(server: SyncServer, default_user, print_tool, default_block):
    """Test agent creation with include_default_source=True"""
    memory_blocks = [CreateBlock(label="human", value="TestUser"), CreateBlock(label="persona", value="I am a test assistant")]

    create_agent_request = CreateAgent(
        name="test_default_source_agent",
        agent_type="memgpt_v2_agent",
        system="test system",
        memory_blocks=memory_blocks,
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        tool_ids=[print_tool.id],
        include_default_source=True,  # This is the key field we're testing
        include_base_tools=False,
    )

    # Create the agent
    created_agent = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )

    # Verify agent was created
    assert created_agent is not None
    assert created_agent.name == "test_default_source_agent"

    # Verify that a default source was created and attached
    attached_sources = await server.agent_manager.list_attached_sources_async(agent_id=created_agent.id, actor=default_user)

    # Should have exactly one source (the default one)
    assert len(attached_sources) == 1
    auto_default_source = attached_sources[0]

    # Verify the default source properties
    assert created_agent.name in auto_default_source.name
    assert auto_default_source.embedding_config.embedding_endpoint_type == "openai"

    # Test with include_default_source=False
    create_agent_request_no_source = CreateAgent(
        name="test_no_default_source_agent",
        agent_type="memgpt_v2_agent",
        system="test system",
        memory_blocks=memory_blocks,
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        tool_ids=[print_tool.id],
        include_default_source=False,  # Explicitly set to False
        include_base_tools=False,
    )

    created_agent_no_source = await server.agent_manager.create_agent_async(
        create_agent_request_no_source,
        actor=default_user,
    )

    # Verify no sources are attached
    attached_sources_no_source = await server.agent_manager.list_attached_sources_async(
        agent_id=created_agent_no_source.id, actor=default_user
    )

    assert len(attached_sources_no_source) == 0

    # Clean up
    await server.agent_manager.delete_agent_async(created_agent.id, default_user)
    await server.agent_manager.delete_agent_async(created_agent_no_source.id, default_user)


async def test_get_context_window_basic(
    server: SyncServer, comprehensive_test_agent_fixture, default_user, default_file, set_letta_environment
):
    # Test agent creation
    created_agent, _create_agent_request = comprehensive_test_agent_fixture

    # Attach a file
    assoc, _closed_files = await server.file_agent_manager.attach_file(
        agent_id=created_agent.id,
        file_id=default_file.id,
        file_name=default_file.file_name,
        source_id=default_file.source_id,
        actor=default_user,
        visible_content="hello",
        max_files_open=created_agent.max_files_open,
    )

    # Get context window and check for basic appearances
    context_window_overview = await server.agent_manager.get_context_window(agent_id=created_agent.id, actor=default_user)
    validate_context_window_overview(created_agent, context_window_overview, assoc)

    # Test deleting the agent
    await server.agent_manager.delete_agent_async(created_agent.id, default_user)
    list_agents = await server.agent_manager.list_agents_async(actor=default_user)
    assert len(list_agents) == 0


@pytest.mark.asyncio
async def test_create_agent_passed_in_initial_messages(server: SyncServer, default_user, default_block):
    memory_blocks = [CreateBlock(label="human", value="BananaBoy"), CreateBlock(label="persona", value="I am a helpful assistant")]
    create_agent_request = CreateAgent(
        agent_type="memgpt_v2_agent",
        system="test system",
        memory_blocks=memory_blocks,
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        tags=["a", "b"],
        description="test_description",
        initial_message_sequence=[MessageCreate(role=MessageRole.user, content="hello world")],
        include_base_tools=False,
    )
    agent_state = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )
    assert await server.message_manager.size_async(agent_id=agent_state.id, actor=default_user) == 2
    init_messages = await server.message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=default_user)

    # Check that the system appears in the first initial message
    assert create_agent_request.system in init_messages[0].content[0].text
    assert create_agent_request.memory_blocks[0].value in init_messages[0].content[0].text
    # Check that the second message is the passed in initial message seq
    assert create_agent_request.initial_message_sequence[0].role == init_messages[1].role
    assert create_agent_request.initial_message_sequence[0].content in init_messages[1].content[0].text


@pytest.mark.asyncio
async def test_create_agent_default_initial_message(server: SyncServer, default_user, default_block):
    memory_blocks = [CreateBlock(label="human", value="BananaBoy"), CreateBlock(label="persona", value="I am a helpful assistant")]
    create_agent_request = CreateAgent(
        agent_type="memgpt_v2_agent",
        system="test system",
        memory_blocks=memory_blocks,
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        tags=["a", "b"],
        description="test_description",
        include_base_tools=False,
    )
    agent_state = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )
    assert await server.message_manager.size_async(agent_id=agent_state.id, actor=default_user) == 4
    init_messages = await server.message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=default_user)
    # Check that the system appears in the first initial message
    assert create_agent_request.system in init_messages[0].content[0].text
    assert create_agent_request.memory_blocks[0].value in init_messages[0].content[0].text


@pytest.mark.asyncio
async def test_create_agent_with_json_in_system_message(server: SyncServer, default_user, default_block):
    system_prompt = (
        "You are an expert teaching agent with encyclopedic knowledge. "
        "When you receive a topic, query the external database for more "
        "information. Format the queries as a JSON list of queries making "
        "sure to include your reasoning for that query, e.g. "
        "{'query1' : 'reason1', 'query2' : 'reason2'}"
    )
    create_agent_request = CreateAgent(
        agent_type="memgpt_v2_agent",
        system=system_prompt,
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        tags=["a", "b"],
        description="test_description",
        include_base_tools=False,
    )
    agent_state = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )
    assert agent_state is not None
    system_message_id = agent_state.message_ids[0]
    system_message = await server.message_manager.get_message_by_id_async(message_id=system_message_id, actor=default_user)
    assert system_prompt in system_message.content[0].text
    assert default_block.value in system_message.content[0].text
    await server.agent_manager.delete_agent_async(agent_id=agent_state.id, actor=default_user)


async def test_update_agent(server: SyncServer, comprehensive_test_agent_fixture, other_tool, other_source, other_block, default_user):
    agent, _ = comprehensive_test_agent_fixture
    update_agent_request = UpdateAgent(
        name="train_agent",
        description="train description",
        tool_ids=[other_tool.id],
        source_ids=[other_source.id],
        block_ids=[other_block.id],
        tool_rules=[InitToolRule(tool_name=other_tool.name)],
        tags=["c", "d"],
        system="train system",
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=EmbeddingConfig.default_config(model_name="letta"),
        message_ids=[f"message-{uuid.uuid4()}", f"message-{uuid.uuid4()}"],
        metadata={"train_key": "train_value"},
        tool_exec_environment_variables={"test_env_var_key_a": "a", "new_tool_exec_key": "n"},
        message_buffer_autoclear=False,
    )

    last_updated_timestamp = agent.updated_at
    updated_agent = await server.agent_manager.update_agent_async(agent.id, update_agent_request, actor=default_user)
    comprehensive_agent_checks(updated_agent, update_agent_request, actor=default_user)
    assert updated_agent.message_ids == update_agent_request.message_ids
    assert updated_agent.updated_at > last_updated_timestamp


@pytest.mark.asyncio
async def test_create_agent_with_compaction_settings(server: SyncServer, default_user, default_block):
    """Test that agents can be created with custom compaction_settings"""
    # Upsert base tools
    await server.tool_manager.upsert_base_tools_async(actor=default_user)

    # Create custom compaction settings
    llm_config = LLMConfig.default_config("gpt-4o-mini")
    model_settings = llm_config._to_model_settings()

    compaction_settings = CompactionSettings(
        model="openai/gpt-4o-mini",
        model_settings=model_settings,
        prompt="Custom summarization prompt",
        clip_chars=1500,
        mode="all",
        sliding_window_percentage=0.5,
    )

    # Create agent with compaction settings
    create_agent_request = CreateAgent(
        name="test_compaction_agent",
        agent_type="memgpt_v2_agent",
        system="test system",
        llm_config=llm_config,
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        include_base_tools=True,
        compaction_settings=compaction_settings,
    )

    created_agent = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )

    # Verify compaction settings were stored correctly
    assert created_agent.compaction_settings is not None
    assert created_agent.compaction_settings.mode == "all"
    assert created_agent.compaction_settings.clip_chars == 1500
    assert created_agent.compaction_settings.sliding_window_percentage == 0.5
    assert created_agent.compaction_settings.prompt == "Custom summarization prompt"

    # Clean up
    await server.agent_manager.delete_agent_async(agent_id=created_agent.id, actor=default_user)


@pytest.mark.asyncio
async def test_update_agent_compaction_settings(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    """Test that an agent's compaction_settings can be updated"""
    agent, _ = comprehensive_test_agent_fixture

    # Create new compaction settings
    llm_config = LLMConfig.default_config("gpt-4o-mini")
    model_settings = llm_config._to_model_settings()

    new_compaction_settings = CompactionSettings(
        model="openai/gpt-4o-mini",
        model_settings=model_settings,
        prompt="Updated summarization prompt",
        prompt_acknowledgement=False,
        clip_chars=3000,
        mode="sliding_window",
        sliding_window_percentage=0.4,
    )

    # Update agent with compaction settings
    update_agent_request = UpdateAgent(
        compaction_settings=new_compaction_settings,
    )

    updated_agent = await server.agent_manager.update_agent_async(agent.id, update_agent_request, actor=default_user)

    # Verify compaction settings were updated correctly
    assert updated_agent.compaction_settings is not None
    assert updated_agent.compaction_settings.mode == "sliding_window"
    assert updated_agent.compaction_settings.clip_chars == 3000
    assert updated_agent.compaction_settings.sliding_window_percentage == 0.4
    assert updated_agent.compaction_settings.prompt == "Updated summarization prompt"
    assert updated_agent.compaction_settings.prompt_acknowledgement == False


@pytest.mark.asyncio
async def test_update_agent_partial_compaction_settings(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    """Test that an agent's compaction_settings can be upserted."""
    from letta.services.summarizer.summarizer_config import get_default_prompt_for_mode

    agent, _ = comprehensive_test_agent_fixture

    # Create new compaction settings
    original_compaction_settings = agent.compaction_settings.model_copy()

    new_compaction_settings = CompactionSettings(
        mode="all",
        prompt_acknowledgement=True,
        clip_chars=3000,
    )

    # Update agent with compaction settings
    update_agent_request = UpdateAgent(
        compaction_settings=new_compaction_settings,
    )

    updated_agent = await server.agent_manager.update_agent_async(agent.id, update_agent_request, actor=default_user)

    # Verify compaction settings were updated correctly
    assert updated_agent.compaction_settings is not None
    assert updated_agent.compaction_settings.model == original_compaction_settings.model
    assert updated_agent.compaction_settings.model_settings == original_compaction_settings.model_settings
    assert updated_agent.compaction_settings.sliding_window_percentage == original_compaction_settings.sliding_window_percentage
    assert updated_agent.compaction_settings.mode == "all"
    assert updated_agent.compaction_settings.clip_chars == 3000
    assert updated_agent.compaction_settings.prompt == get_default_prompt_for_mode("all")
    assert updated_agent.compaction_settings.prompt_acknowledgement == True


@pytest.mark.asyncio
async def test_update_agent_partial_compaction_settings_same_mode(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    """Test that if the mode stays the same without a prompt passed in, the prompt is not updated."""

    agent, _ = comprehensive_test_agent_fixture

    update_agent_request = UpdateAgent(
        compaction_settings=CompactionSettings(mode="sliding_window", prompt="This is a fake prompt."),
    )
    updated_agent = await server.agent_manager.update_agent_async(agent.id, update_agent_request, actor=default_user)

    assert updated_agent.compaction_settings is not None
    assert updated_agent.compaction_settings.prompt == "This is a fake prompt."

    # Create new compaction settings
    original_compaction_settings = updated_agent.compaction_settings.model_copy()

    new_compaction_settings = CompactionSettings(
        mode="sliding_window",
        model="openai/gpt-4o-mini",
    )

    # Update agent with compaction settings
    update_agent_request = UpdateAgent(
        compaction_settings=new_compaction_settings,
    )

    final_agent = await server.agent_manager.update_agent_async(updated_agent.id, update_agent_request, actor=default_user)

    # Verify compaction settings were updated correctly
    assert final_agent.compaction_settings is not None
    assert final_agent.compaction_settings.sliding_window_percentage == original_compaction_settings.sliding_window_percentage
    assert final_agent.compaction_settings.prompt == original_compaction_settings.prompt
    assert final_agent.compaction_settings.clip_chars == original_compaction_settings.clip_chars
    assert final_agent.compaction_settings.prompt_acknowledgement == original_compaction_settings.prompt_acknowledgement
    assert final_agent.compaction_settings.mode == "sliding_window"
    assert final_agent.compaction_settings.model == "openai/gpt-4o-mini"


@pytest.mark.asyncio
async def test_agent_file_defaults_based_on_context_window(server: SyncServer, default_user, default_block):
    """Test that file-related defaults are set based on the model's context window size"""

    # test with small context window model (8k)
    llm_config_small = LLMConfig.default_config("gpt-4o-mini")
    llm_config_small.context_window = 8000
    create_agent_request = CreateAgent(
        name="test_agent_small_context",
        agent_type="memgpt_v2_agent",
        llm_config=llm_config_small,
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        include_base_tools=False,
    )
    agent_state = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )
    assert agent_state.max_files_open == 3
    assert (
        agent_state.per_file_view_window_char_limit == calculate_file_defaults_based_on_context_window(llm_config_small.context_window)[1]
    )
    await server.agent_manager.delete_agent_async(agent_id=agent_state.id, actor=default_user)

    # test with medium context window model (32k)
    llm_config_medium = LLMConfig.default_config("gpt-4o-mini")
    llm_config_medium.context_window = 32000
    create_agent_request = CreateAgent(
        name="test_agent_medium_context",
        agent_type="memgpt_v2_agent",
        llm_config=llm_config_medium,
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        include_base_tools=False,
    )
    agent_state = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )
    assert agent_state.max_files_open == 5
    assert (
        agent_state.per_file_view_window_char_limit == calculate_file_defaults_based_on_context_window(llm_config_medium.context_window)[1]
    )
    await server.agent_manager.delete_agent_async(agent_id=agent_state.id, actor=default_user)

    # test with large context window model (128k)
    llm_config_large = LLMConfig.default_config("gpt-4o-mini")
    llm_config_large.context_window = 128000
    create_agent_request = CreateAgent(
        name="test_agent_large_context",
        agent_type="memgpt_v2_agent",
        llm_config=llm_config_large,
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        include_base_tools=False,
    )
    agent_state = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )
    assert agent_state.max_files_open == 10
    assert (
        agent_state.per_file_view_window_char_limit == calculate_file_defaults_based_on_context_window(llm_config_large.context_window)[1]
    )
    await server.agent_manager.delete_agent_async(agent_id=agent_state.id, actor=default_user)


@pytest.mark.asyncio
async def test_agent_file_defaults_explicit_values(server: SyncServer, default_user, default_block):
    """Test that explicitly set file-related values are respected"""

    llm_config_explicit = LLMConfig.default_config("gpt-4o-mini")
    llm_config_explicit.context_window = 32000  # would normally get defaults of 5 and 30k
    create_agent_request = CreateAgent(
        name="test_agent_explicit_values",
        agent_type="memgpt_v2_agent",
        llm_config=llm_config_explicit,
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        include_base_tools=False,
        max_files_open=20,  # explicit value
        per_file_view_window_char_limit=500_000,  # explicit value
    )
    agent_state = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )
    # verify explicit values are used instead of defaults
    assert agent_state.max_files_open == 20
    assert agent_state.per_file_view_window_char_limit == 500_000
    await server.agent_manager.delete_agent_async(agent_id=agent_state.id, actor=default_user)


@pytest.mark.asyncio
async def test_update_agent_file_fields(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    """Test updating file-related fields on an existing agent"""

    agent, _ = comprehensive_test_agent_fixture

    # update file-related fields
    update_request = UpdateAgent(
        max_files_open=15,
        per_file_view_window_char_limit=150_000,
    )
    updated_agent = await server.agent_manager.update_agent_async(agent.id, update_request, actor=default_user)

    assert updated_agent.max_files_open == 15
    assert updated_agent.per_file_view_window_char_limit == 150_000


@pytest.mark.asyncio
async def test_update_agent_last_stop_reason(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    """Test updating last_stop_reason field on an existing agent"""

    agent, _ = comprehensive_test_agent_fixture

    assert agent.last_stop_reason is None

    # Update with end_turn stop reason
    update_request = UpdateAgent(
        last_stop_reason=StopReasonType.end_turn,
        last_run_completion=datetime.now(timezone.utc),
        last_run_duration_ms=1500,
    )
    updated_agent = await server.agent_manager.update_agent_async(agent.id, update_request, actor=default_user)

    assert updated_agent.last_stop_reason == StopReasonType.end_turn
    assert updated_agent.last_run_completion is not None
    assert updated_agent.last_run_duration_ms == 1500

    # Update with error stop reason
    update_request = UpdateAgent(
        last_stop_reason=StopReasonType.error,
        last_run_completion=datetime.now(timezone.utc),
        last_run_duration_ms=2500,
    )
    updated_agent = await server.agent_manager.update_agent_async(agent.id, update_request, actor=default_user)

    assert updated_agent.last_stop_reason == StopReasonType.error
    assert updated_agent.last_run_duration_ms == 2500

    # Update with requires_approval stop reason
    update_request = UpdateAgent(
        last_stop_reason=StopReasonType.requires_approval,
    )
    updated_agent = await server.agent_manager.update_agent_async(agent.id, update_request, actor=default_user)

    assert updated_agent.last_stop_reason == StopReasonType.requires_approval


# ======================================================================================================================
# AgentManager Tests - Listing
# ======================================================================================================================


@pytest.mark.asyncio
async def test_list_agents_select_fields_empty(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    # Create an agent using the comprehensive fixture.
    _created_agent, _create_agent_request = comprehensive_test_agent_fixture

    # List agents using an empty list for select_fields.
    agents = await server.agent_manager.list_agents_async(actor=default_user, include_relationships=[])
    # Assert that the agent is returned and basic fields are present.
    assert len(agents) >= 1
    agent = agents[0]
    assert agent.id is not None
    assert agent.name is not None

    # Assert no relationships were loaded
    assert len(agent.tools) == 0
    assert len(agent.tags) == 0


@pytest.mark.asyncio
async def test_list_agents_select_fields_none(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    # Create an agent using the comprehensive fixture.
    _created_agent, _create_agent_request = comprehensive_test_agent_fixture

    # List agents using an empty list for select_fields.
    agents = await server.agent_manager.list_agents_async(actor=default_user, include_relationships=None)
    # Assert that the agent is returned and basic fields are present.
    assert len(agents) >= 1
    agent = agents[0]
    assert agent.id is not None
    assert agent.name is not None

    # Assert no relationships were loaded
    assert len(agent.tools) > 0
    assert len(agent.tags) > 0


@pytest.mark.asyncio
async def test_list_agents_select_fields_specific(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    _created_agent, _create_agent_request = comprehensive_test_agent_fixture

    # Choose a subset of valid relationship fields.
    valid_fields = ["tools", "tags"]
    agents = await server.agent_manager.list_agents_async(actor=default_user, include_relationships=valid_fields)
    assert len(agents) >= 1
    agent = agents[0]
    # Depending on your to_pydantic() implementation,
    # verify that the fields exist in the returned pydantic model.
    # (Note: These assertions may require that your CreateAgent fixture sets up these relationships.)
    assert agent.tools
    assert sorted(agent.tags) == ["a", "b"]
    assert not agent.memory.blocks


@pytest.mark.asyncio
async def test_list_agents_select_fields_invalid(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    _created_agent, _create_agent_request = comprehensive_test_agent_fixture

    # Provide field names that are not recognized.
    invalid_fields = ["foobar", "nonexistent_field"]
    # The expectation is that these fields are simply ignored.
    agents = await server.agent_manager.list_agents_async(actor=default_user, include_relationships=invalid_fields)
    assert len(agents) >= 1
    agent = agents[0]
    # Verify that standard fields are still present.c
    assert agent.id is not None
    assert agent.name is not None


@pytest.mark.asyncio
async def test_list_agents_select_fields_duplicates(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    _created_agent, _create_agent_request = comprehensive_test_agent_fixture

    # Provide duplicate valid field names.
    duplicate_fields = ["tools", "tools", "tags", "tags"]
    agents = await server.agent_manager.list_agents_async(actor=default_user, include_relationships=duplicate_fields)
    assert len(agents) >= 1
    agent = agents[0]
    # Verify that the agent pydantic representation includes the relationships.
    # Even if duplicates were provided, the query should not break.
    assert isinstance(agent.tools, list)
    assert isinstance(agent.tags, list)


@pytest.mark.asyncio
async def test_list_agents_select_fields_mixed(server: SyncServer, comprehensive_test_agent_fixture, default_user):
    _created_agent, _create_agent_request = comprehensive_test_agent_fixture

    # Mix valid fields with an invalid one.
    mixed_fields = ["tools", "invalid_field"]
    agents = await server.agent_manager.list_agents_async(actor=default_user, include_relationships=mixed_fields)
    assert len(agents) >= 1
    agent = agents[0]
    # Valid fields should be loaded and accessible.
    assert agent.tools
    # Since "invalid_field" is not recognized, it should have no adverse effect.
    # You might optionally check that no extra attribute is created on the pydantic model.
    assert not hasattr(agent, "invalid_field")


@pytest.mark.asyncio
async def test_list_agents_ascending(server: SyncServer, default_user):
    # Create two agents with known names
    await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_oldest",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
        ),
        actor=default_user,
    )

    if USING_SQLITE:
        time.sleep(CREATE_DELAY_SQLITE)

    await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_newest",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
        ),
        actor=default_user,
    )

    agents = await server.agent_manager.list_agents_async(actor=default_user, ascending=True)
    names = [agent.name for agent in agents]
    assert names.index("agent_oldest") < names.index("agent_newest")


@pytest.mark.asyncio
async def test_list_agents_descending(server: SyncServer, default_user):
    # Create two agents with known names
    await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_oldest",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
        ),
        actor=default_user,
    )

    if USING_SQLITE:
        time.sleep(CREATE_DELAY_SQLITE)

    await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_newest",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
        ),
        actor=default_user,
    )

    agents = await server.agent_manager.list_agents_async(actor=default_user, ascending=False)
    names = [agent.name for agent in agents]
    assert names.index("agent_newest") < names.index("agent_oldest")


@pytest.mark.asyncio
async def test_list_agents_by_last_stop_reason(server: SyncServer, default_user):
    # Create agent with requires_approval stop reason
    agent1 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_requires_approval",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
        ),
        actor=default_user,
    )
    await server.agent_manager.update_agent_async(
        agent_id=agent1.id,
        agent_update=UpdateAgent(last_stop_reason=StopReasonType.requires_approval),
        actor=default_user,
    )

    # Create agent with error stop reason
    agent2 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_error",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
        ),
        actor=default_user,
    )
    await server.agent_manager.update_agent_async(
        agent_id=agent2.id,
        agent_update=UpdateAgent(last_stop_reason=StopReasonType.error),
        actor=default_user,
    )

    # Create agent with no stop reason
    await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_no_stop_reason",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
        ),
        actor=default_user,
    )

    # Filter by requires_approval
    approval_agents = await server.agent_manager.list_agents_async(
        actor=default_user, last_stop_reason=StopReasonType.requires_approval.value
    )
    approval_names = {agent.name for agent in approval_agents}
    assert approval_names == {"agent_requires_approval"}

    # Filter by error
    error_agents = await server.agent_manager.list_agents_async(actor=default_user, last_stop_reason=StopReasonType.error.value)
    error_names = {agent.name for agent in error_agents}
    assert error_names == {"agent_error"}

    # No filter - should return all agents
    all_agents = await server.agent_manager.list_agents_async(actor=default_user)
    all_names = {agent.name for agent in all_agents}
    assert {"agent_requires_approval", "agent_error", "agent_no_stop_reason"}.issubset(all_names)


@pytest.mark.asyncio
async def test_count_agents_with_filters(server: SyncServer, default_user):
    """Test count_agents_async with various filters"""
    # Create agents with different attributes
    agent1 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_requires_approval",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
            tags=["inbox", "test"],
        ),
        actor=default_user,
    )
    await server.agent_manager.update_agent_async(
        agent_id=agent1.id,
        agent_update=UpdateAgent(last_stop_reason=StopReasonType.requires_approval),
        actor=default_user,
    )

    agent2 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_error",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
            tags=["error", "test"],
        ),
        actor=default_user,
    )
    await server.agent_manager.update_agent_async(
        agent_id=agent2.id,
        agent_update=UpdateAgent(last_stop_reason=StopReasonType.error),
        actor=default_user,
    )

    agent3 = await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_completed",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
            tags=["completed"],
        ),
        actor=default_user,
    )
    await server.agent_manager.update_agent_async(
        agent_id=agent3.id,
        agent_update=UpdateAgent(last_stop_reason=StopReasonType.end_turn),
        actor=default_user,
    )

    await server.agent_manager.create_agent_async(
        agent_create=CreateAgent(
            name="agent_no_stop_reason",
            agent_type="memgpt_v2_agent",
            llm_config=LLMConfig.default_config("gpt-4o-mini"),
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
            memory_blocks=[],
            include_base_tools=False,
            tags=["test"],
        ),
        actor=default_user,
    )

    # Test count with no filters - should return total count
    total_count = await server.agent_manager.count_agents_async(actor=default_user)
    assert total_count >= 4

    # Test count by last_stop_reason - requires_approval (inbox use case)
    approval_count = await server.agent_manager.count_agents_async(
        actor=default_user, last_stop_reason=StopReasonType.requires_approval.value
    )
    assert approval_count == 1

    # Test count by last_stop_reason - error
    error_count = await server.agent_manager.count_agents_async(actor=default_user, last_stop_reason=StopReasonType.error.value)
    assert error_count == 1

    # Test count by last_stop_reason - end_turn
    completed_count = await server.agent_manager.count_agents_async(actor=default_user, last_stop_reason=StopReasonType.end_turn.value)
    assert completed_count == 1

    # Test count by tags
    test_tag_count = await server.agent_manager.count_agents_async(actor=default_user, tags=["test"])
    assert test_tag_count == 3

    # Test count by tags with match_all_tags
    inbox_test_count = await server.agent_manager.count_agents_async(actor=default_user, tags=["inbox", "test"], match_all_tags=True)
    assert inbox_test_count == 1

    # Test count by name
    name_count = await server.agent_manager.count_agents_async(actor=default_user, name="agent_requires_approval")
    assert name_count == 1

    # Test count by query_text
    query_count = await server.agent_manager.count_agents_async(actor=default_user, query_text="error")
    assert query_count >= 1

    # Test combined filters: last_stop_reason + tags
    combined_count = await server.agent_manager.count_agents_async(
        actor=default_user, last_stop_reason=StopReasonType.requires_approval.value, tags=["inbox"]
    )
    assert combined_count == 1


@pytest.mark.asyncio
async def test_list_agents_ordering_and_pagination(server: SyncServer, default_user):
    names = ["alpha_agent", "beta_agent", "gamma_agent"]
    created_agents = []

    # Create agents in known order
    for name in names:
        agent = await server.agent_manager.create_agent_async(
            agent_create=CreateAgent(
                name=name,
                agent_type="memgpt_v2_agent",
                memory_blocks=[],
                llm_config=LLMConfig.default_config("gpt-4o-mini"),
                embedding_config=EmbeddingConfig.default_config(provider="openai"),
                include_base_tools=False,
            ),
            actor=default_user,
        )
        created_agents.append(agent)
        if USING_SQLITE:
            time.sleep(CREATE_DELAY_SQLITE)

    agent_ids = {agent.name: agent.id for agent in created_agents}

    # Ascending (oldest to newest)
    agents_asc = await server.agent_manager.list_agents_async(actor=default_user, ascending=True)
    asc_names = [agent.name for agent in agents_asc]
    assert asc_names.index("alpha_agent") < asc_names.index("beta_agent") < asc_names.index("gamma_agent")

    # Descending (newest to oldest)
    agents_desc = await server.agent_manager.list_agents_async(actor=default_user, ascending=False)
    desc_names = [agent.name for agent in agents_desc]
    assert desc_names.index("gamma_agent") < desc_names.index("beta_agent") < desc_names.index("alpha_agent")

    # After: Get agents after alpha_agent in ascending order (should exclude alpha)
    after_alpha = await server.agent_manager.list_agents_async(actor=default_user, after=agent_ids["alpha_agent"], ascending=True)
    after_names = [a.name for a in after_alpha]
    assert "alpha_agent" not in after_names
    assert "beta_agent" in after_names
    assert "gamma_agent" in after_names
    assert after_names == ["beta_agent", "gamma_agent"]

    # Before: Get agents before gamma_agent in ascending order (should exclude gamma)
    before_gamma = await server.agent_manager.list_agents_async(actor=default_user, before=agent_ids["gamma_agent"], ascending=True)
    before_names = [a.name for a in before_gamma]
    assert "gamma_agent" not in before_names
    assert "alpha_agent" in before_names
    assert "beta_agent" in before_names
    assert before_names == ["alpha_agent", "beta_agent"]

    # After: Get agents after gamma_agent in descending order (should exclude gamma, return beta then alpha)
    after_gamma_desc = await server.agent_manager.list_agents_async(actor=default_user, after=agent_ids["gamma_agent"], ascending=False)
    after_names_desc = [a.name for a in after_gamma_desc]
    assert after_names_desc == ["beta_agent", "alpha_agent"]

    # Before: Get agents before alpha_agent in descending order (should exclude alpha)
    before_alpha_desc = await server.agent_manager.list_agents_async(actor=default_user, before=agent_ids["alpha_agent"], ascending=False)
    before_names_desc = [a.name for a in before_alpha_desc]
    assert before_names_desc == ["gamma_agent", "beta_agent"]


# ======================================================================================================================
# AgentManager Tests - Environment Variable Encryption
# ======================================================================================================================


@pytest.fixture
def encryption_key():
    """Fixture to ensure encryption key is set for tests."""
    original_key = settings.encryption_key
    # Set a test encryption key if not already set
    if not settings.encryption_key:
        settings.encryption_key = "test-encryption-key-32-bytes!!"
    yield settings.encryption_key
    # Restore original
    settings.encryption_key = original_key


@pytest.mark.asyncio
async def test_agent_environment_variables_encrypt_on_create(server: SyncServer, default_user, encryption_key):
    """Test that creating an agent with secrets encrypts the values in the database."""
    from letta.orm.sandbox_config import AgentEnvironmentVariable as AgentEnvironmentVariableModel
    from letta.schemas.secret import Secret

    # Create agent with secrets
    agent_create = CreateAgent(
        name="test-agent-with-secrets",
        agent_type="memgpt_v2_agent",
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        include_base_tools=False,
        secrets={
            "API_KEY": "sk-test-secret-12345",
            "DATABASE_URL": "postgres://user:pass@localhost/db",
        },
    )

    created_agent = await server.agent_manager.create_agent_async(agent_create, actor=default_user)

    # Verify agent has secrets
    assert created_agent.secrets is not None
    assert len(created_agent.secrets) == 2

    # Verify secrets are AgentEnvironmentVariable objects with Secret fields
    for secret_obj in created_agent.secrets:
        assert secret_obj.key in ["API_KEY", "DATABASE_URL"]
        assert secret_obj.value_enc is not None
        assert isinstance(secret_obj.value_enc, Secret)

    # Verify values are encrypted in the database
    async with db_registry.async_session() as session:
        env_vars = await session.execute(
            select(AgentEnvironmentVariableModel).where(AgentEnvironmentVariableModel.agent_id == created_agent.id)
        )
        env_var_list = list(env_vars.scalars().all())

        assert len(env_var_list) == 2
        for env_var in env_var_list:
            # Check that value_enc is not None and is encrypted
            assert env_var.value_enc is not None
            assert isinstance(env_var.value_enc, str)

            # Decrypt and verify
            decrypted = Secret.from_encrypted(env_var.value_enc).get_plaintext()
            if env_var.key == "API_KEY":
                assert decrypted == "sk-test-secret-12345"
            elif env_var.key == "DATABASE_URL":
                assert decrypted == "postgres://user:pass@localhost/db"


@pytest.mark.asyncio
async def test_agent_environment_variables_decrypt_on_read(server: SyncServer, default_user, encryption_key):
    """Test that reading an agent deserializes secrets correctly to AgentEnvironmentVariable objects."""
    from letta.schemas.environment_variables import AgentEnvironmentVariable
    from letta.schemas.secret import Secret

    # Create agent with secrets
    agent_create = CreateAgent(
        name="test-agent-read-secrets",
        agent_type="memgpt_v2_agent",
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        include_base_tools=False,
        secrets={
            "TEST_KEY": "test-value-67890",
        },
    )

    created_agent = await server.agent_manager.create_agent_async(agent_create, actor=default_user)
    agent_id = created_agent.id

    # Read the agent back
    retrieved_agent = await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=default_user)

    # Verify secrets are properly deserialized
    assert retrieved_agent.secrets is not None
    assert len(retrieved_agent.secrets) == 1

    secret_obj = retrieved_agent.secrets[0]
    assert isinstance(secret_obj, AgentEnvironmentVariable)
    assert secret_obj.key == "TEST_KEY"
    assert secret_obj.value == "test-value-67890"

    # Verify value_enc is a Secret object (not a string)
    assert secret_obj.value_enc is not None
    assert isinstance(secret_obj.value_enc, Secret)

    # Verify we can decrypt through the Secret object
    decrypted = secret_obj.value_enc.get_plaintext()
    assert decrypted == "test-value-67890"

    # Verify direct value_enc access works
    assert isinstance(secret_obj.value_enc, Secret)
    assert secret_obj.value_enc.get_plaintext() == "test-value-67890"


@pytest.mark.asyncio
async def test_agent_environment_variables_update_encryption(server: SyncServer, default_user, encryption_key):
    """Test that updating agent secrets encrypts new values."""
    from letta.orm.sandbox_config import AgentEnvironmentVariable as AgentEnvironmentVariableModel
    from letta.schemas.secret import Secret

    # Create agent with initial secrets
    agent_create = CreateAgent(
        name="test-agent-update-secrets",
        agent_type="memgpt_v2_agent",
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        include_base_tools=False,
        secrets={
            "INITIAL_KEY": "initial-value",
        },
    )

    created_agent = await server.agent_manager.create_agent_async(agent_create, actor=default_user)
    agent_id = created_agent.id

    # Update with new secrets
    agent_update = UpdateAgent(
        secrets={
            "UPDATED_KEY": "updated-value-abc",
            "NEW_KEY": "new-value-xyz",
        },
    )

    updated_agent = await server.agent_manager.update_agent_async(agent_id=agent_id, agent_update=agent_update, actor=default_user)

    # Verify updated secrets
    assert updated_agent.secrets is not None
    assert len(updated_agent.secrets) == 2

    # Verify in database
    async with db_registry.async_session() as session:
        env_vars = await session.execute(select(AgentEnvironmentVariableModel).where(AgentEnvironmentVariableModel.agent_id == agent_id))
        env_var_list = list(env_vars.scalars().all())

        assert len(env_var_list) == 2
        for env_var in env_var_list:
            assert env_var.value_enc is not None

            # Decrypt and verify
            decrypted = Secret.from_encrypted(env_var.value_enc).get_plaintext()
            if env_var.key == "UPDATED_KEY":
                assert decrypted == "updated-value-abc"
            elif env_var.key == "NEW_KEY":
                assert decrypted == "new-value-xyz"
            else:
                pytest.fail(f"Unexpected key: {env_var.key}")


@pytest.mark.asyncio
async def test_agent_secrets_clear_with_empty_dict(server: SyncServer, default_user, encryption_key):
    """Test that updating agent secrets with empty dict clears all secrets."""
    from letta.orm.sandbox_config import AgentEnvironmentVariable as AgentEnvironmentVariableModel

    # Create agent with initial secrets
    agent_create = CreateAgent(
        name="test-agent-clear-secrets",
        agent_type="memgpt_v2_agent",
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=DEFAULT_EMBEDDING_CONFIG,
        include_base_tools=False,
        secrets={
            "SECRET_KEY_1": "secret-value-1",
            "SECRET_KEY_2": "secret-value-2",
        },
    )

    created_agent = await server.agent_manager.create_agent_async(agent_create, actor=default_user)
    agent_id = created_agent.id

    # Verify secrets were created
    assert created_agent.secrets is not None
    assert len(created_agent.secrets) == 2

    # Update with empty dict to clear all secrets
    agent_update = UpdateAgent(secrets={})
    updated_agent = await server.agent_manager.update_agent_async(agent_id=agent_id, agent_update=agent_update, actor=default_user)

    # Verify secrets are cleared
    assert updated_agent.secrets is not None
    assert len(updated_agent.secrets) == 0

    # Verify in database
    async with db_registry.async_session() as session:
        env_vars = await session.execute(select(AgentEnvironmentVariableModel).where(AgentEnvironmentVariableModel.agent_id == agent_id))
        env_var_list = list(env_vars.scalars().all())
        assert len(env_var_list) == 0


@pytest.mark.asyncio
async def test_agent_state_schema_unchanged(server: SyncServer):
    """
    Test that the AgentState pydantic schema structure has not changed.
    This test validates all fields including nested pydantic objects to ensure
    the schema remains stable across changes.
    """
    from letta.schemas.agent import AgentState, AgentType
    from letta.schemas.block import Block
    from letta.schemas.embedding_config import EmbeddingConfig
    from letta.schemas.environment_variables import AgentEnvironmentVariable
    from letta.schemas.group import Group
    from letta.schemas.letta_message import ApprovalRequestMessage
    from letta.schemas.llm_config import LLMConfig
    from letta.schemas.memory import Memory
    from letta.schemas.model import ModelSettingsUnion
    from letta.schemas.response_format import ResponseFormatUnion
    from letta.schemas.source import Source
    from letta.schemas.tool import Tool
    from letta.services.summarizer.summarizer_config import CompactionSettings

    # Define the expected schema structure
    expected_schema = {
        # Core identification
        "id": str,
        "name": str,
        # Tool rules
        "tool_rules": (list, type(None)),
        # In-context memory
        "message_ids": (list, type(None)),
        # System prompt
        "system": str,
        # Agent configuration
        "agent_type": AgentType,
        # LLM information
        "llm_config": LLMConfig,
        "compaction_settings": CompactionSettings,
        "model": str,
        "embedding": str,
        "embedding_config": EmbeddingConfig,
        "model_settings": (ModelSettingsUnion, type(None)),
        "response_format": (ResponseFormatUnion, type(None)),
        # State fields
        "description": (str, type(None)),
        "metadata": (dict, type(None)),
        # Memory and tools
        "memory": Memory,  # deprecated
        "blocks": list,
        "tools": list,
        "sources": list,
        "tags": list,
        "tool_exec_environment_variables": list,  # deprecated
        "secrets": list,
        # Project and template fields
        "project_id": (str, type(None)),
        "template_id": (str, type(None)),
        "base_template_id": (str, type(None)),
        "deployment_id": (str, type(None)),
        "entity_id": (str, type(None)),
        "identity_ids": list,
        "identities": list,
        "pending_approval": (ApprovalRequestMessage, type(None)),
        # Advanced configuration
        "message_buffer_autoclear": bool,
        "enable_sleeptime": (bool, type(None)),
        # Multi-agent
        "multi_agent_group": (Group, type(None)),  # deprecated
        "managed_group": (Group, type(None)),
        # Run metrics
        "last_run_completion": (datetime, type(None)),
        "last_run_duration_ms": (int, type(None)),
        "last_stop_reason": (StopReasonType, type(None)),
        # Timezone
        "timezone": (str, type(None)),
        # File controls
        "max_files_open": (int, type(None)),
        "per_file_view_window_char_limit": (int, type(None)),
        # Indexing controls
        "hidden": (bool, type(None)),
        # Metadata fields (from OrmMetadataBase)
        "created_by_id": (str, type(None)),
        "last_updated_by_id": (str, type(None)),
        "created_at": (datetime, type(None)),
        "updated_at": (datetime, type(None)),
    }

    # Get the actual schema fields from AgentState
    agent_state_fields = AgentState.model_fields
    actual_field_names = set(agent_state_fields.keys())
    expected_field_names = set(expected_schema.keys())

    # Check for added fields
    added_fields = actual_field_names - expected_field_names
    if added_fields:
        pytest.fail(
            f"New fields detected in AgentState schema: {sorted(added_fields)}. "
            "This test must be updated to include these fields, and the schema change must be intentional."
        )

    # Check for removed fields
    removed_fields = expected_field_names - actual_field_names
    if removed_fields:
        pytest.fail(
            f"Fields removed from AgentState schema: {sorted(removed_fields)}. "
            "This test must be updated to remove these fields, and the schema change must be intentional."
        )

    # Validate field types
    import typing

    for field_name, expected_type in expected_schema.items():
        field = agent_state_fields[field_name]
        annotation = field.annotation

        # Helper function to check if annotation matches expected type
        def check_type_match(annotation, expected):
            origin = typing.get_origin(annotation)
            args = typing.get_args(annotation)

            # Direct match
            if annotation == expected:
                return True

            # Handle list type (List[X] should match list)
            if expected is list and origin is list:
                return True

            # Handle dict type (Dict[X, Y] should match dict)
            if expected is dict and origin is dict:
                return True

            # Handle Optional types
            if origin is typing.Union:
                # Check if expected type is in the union
                if expected in args:
                    return True
                # Handle list case within Union (e.g., Union[List[X], None])
                if expected is list:
                    for arg in args:
                        if typing.get_origin(arg) is list:
                            return True
                # Handle dict case within Union
                if expected is dict:
                    for arg in args:
                        if typing.get_origin(arg) is dict:
                            return True
                # Handle Annotated types within Union (e.g., Union[Annotated[...], None])
                # This checks if any of the union args is an Annotated type that matches expected
                for arg in args:
                    if typing.get_origin(arg) is typing.Annotated:
                        # For Annotated types, compare the first argument (the actual type)
                        annotated_args = typing.get_args(arg)
                        if annotated_args and annotated_args[0] == expected:
                            return True

            return False

        # Handle tuple of expected types (Optional)
        if isinstance(expected_type, tuple):
            valid = any(check_type_match(annotation, exp_t) for exp_t in expected_type)
            if not valid:
                pytest.fail(
                    f"Field '{field_name}' type changed. Expected one of {expected_type}, "
                    f"but got {annotation}. Schema changes must be intentional."
                )
        else:
            # Single expected type
            valid = check_type_match(annotation, expected_type)
            if not valid:
                pytest.fail(
                    f"Field '{field_name}' type changed. Expected {expected_type}, "
                    f"but got {annotation}. Schema changes must be intentional."
                )

    # Validate nested object schemas
    # Memory schema
    memory_fields = Memory.model_fields
    expected_memory_fields = {"agent_type", "git_enabled", "blocks", "file_blocks", "prompt_template"}
    actual_memory_fields = set(memory_fields.keys())
    if actual_memory_fields != expected_memory_fields:
        pytest.fail(
            f"Memory schema changed. Expected fields: {expected_memory_fields}, "
            f"Got: {actual_memory_fields}. Schema changes must be intentional."
        )

    # Block schema
    block_fields = Block.model_fields
    expected_block_fields = {
        "id",
        "value",
        "limit",
        "project_id",
        "template_name",
        "is_template",
        "template_id",
        "base_template_id",
        "deployment_id",
        "entity_id",
        "preserve_on_migration",
        "label",
        "read_only",
        "description",
        "metadata",
        "hidden",
        "created_by_id",
        "last_updated_by_id",
        "tags",
    }
    actual_block_fields = set(block_fields.keys())
    if actual_block_fields != expected_block_fields:
        pytest.fail(
            f"Block schema changed. Expected fields: {expected_block_fields}, "
            f"Got: {actual_block_fields}. Schema changes must be intentional."
        )

    # Tool schema
    tool_fields = Tool.model_fields
    expected_tool_fields = {
        "id",
        "tool_type",
        "description",
        "source_type",
        "name",
        "tags",
        "source_code",
        "json_schema",
        "args_json_schema",
        "return_char_limit",
        "pip_requirements",
        "npm_requirements",
        "default_requires_approval",
        "enable_parallel_execution",
        "created_by_id",
        "last_updated_by_id",
        "metadata_",
        "project_id",
    }
    actual_tool_fields = set(tool_fields.keys())
    if actual_tool_fields != expected_tool_fields:
        pytest.fail(
            f"Tool schema changed. Expected fields: {expected_tool_fields}, Got: {actual_tool_fields}. Schema changes must be intentional."
        )

    # Source schema
    source_fields = Source.model_fields
    expected_source_fields = {
        "id",
        "name",
        "description",
        "instructions",
        "metadata",
        "embedding_config",
        "organization_id",
        "vector_db_provider",
        "created_by_id",
        "last_updated_by_id",
        "created_at",
        "updated_at",
    }
    actual_source_fields = set(source_fields.keys())
    if actual_source_fields != expected_source_fields:
        pytest.fail(
            f"Source schema changed. Expected fields: {expected_source_fields}, "
            f"Got: {actual_source_fields}. Schema changes must be intentional."
        )

    # LLMConfig schema
    llm_config_fields = LLMConfig.model_fields
    expected_llm_config_fields = {
        "model",
        "display_name",
        "model_endpoint_type",
        "model_endpoint",
        "provider_name",
        "provider_category",
        "model_wrapper",
        "context_window",
        "put_inner_thoughts_in_kwargs",
        "handle",
        "temperature",
        "max_tokens",
        "enable_reasoner",
        "reasoning_effort",
        "effort",
        "response_format",
        "max_reasoning_tokens",
        "frequency_penalty",
        "compatibility_type",
        "verbosity",
        "tier",
        "parallel_tool_calls",
        "strict",
        "return_logprobs",
        "top_logprobs",
        "return_token_ids",
    }
    actual_llm_config_fields = set(llm_config_fields.keys())
    if actual_llm_config_fields != expected_llm_config_fields:
        pytest.fail(
            f"LLMConfig schema changed. Expected fields: {expected_llm_config_fields}, "
            f"Got: {actual_llm_config_fields}. Schema changes must be intentional."
        )

    # EmbeddingConfig schema
    embedding_config_fields = EmbeddingConfig.model_fields
    expected_embedding_config_fields = {
        "embedding_endpoint_type",
        "embedding_endpoint",
        "embedding_model",
        "embedding_dim",
        "embedding_chunk_size",
        "handle",
        "batch_size",
        "azure_endpoint",
        "azure_version",
        "azure_deployment",
    }
    actual_embedding_config_fields = set(embedding_config_fields.keys())
    if actual_embedding_config_fields != expected_embedding_config_fields:
        pytest.fail(
            f"EmbeddingConfig schema changed. Expected fields: {expected_embedding_config_fields}, "
            f"Got: {actual_embedding_config_fields}. Schema changes must be intentional."
        )

    # AgentEnvironmentVariable schema
    agent_env_var_fields = AgentEnvironmentVariable.model_fields
    expected_agent_env_var_fields = {
        "id",
        "key",
        "value",
        "description",
        "organization_id",
        "value_enc",
        "agent_id",
        # From OrmMetadataBase
        "created_by_id",
        "last_updated_by_id",
        "created_at",
        "updated_at",
    }
    actual_agent_env_var_fields = set(agent_env_var_fields.keys())
    if actual_agent_env_var_fields != expected_agent_env_var_fields:
        pytest.fail(
            f"AgentEnvironmentVariable schema changed. Expected fields: {expected_agent_env_var_fields}, "
            f"Got: {actual_agent_env_var_fields}. Schema changes must be intentional."
        )

    # Group schema
    group_fields = Group.model_fields
    expected_group_fields = {
        "id",
        "manager_type",
        "agent_ids",
        "description",
        "project_id",
        "template_id",
        "base_template_id",
        "deployment_id",
        "shared_block_ids",
        "manager_agent_id",
        "termination_token",
        "max_turns",
        "sleeptime_agent_frequency",
        "turns_counter",
        "last_processed_message_id",
        "max_message_buffer_length",
        "min_message_buffer_length",
        "hidden",
    }
    actual_group_fields = set(group_fields.keys())
    if actual_group_fields != expected_group_fields:
        pytest.fail(
            f"Group schema changed. Expected fields: {expected_group_fields}, "
            f"Got: {actual_group_fields}. Schema changes must be intentional."
        )


async def test_agent_state_relationship_loads(server: SyncServer, default_user, print_tool, default_block):
    memory_blocks = [CreateBlock(label="human", value="TestUser"), CreateBlock(label="persona", value="I am a test assistant")]

    create_agent_request = CreateAgent(
        name="test_default_source_agent",
        agent_type="memgpt_v2_agent",
        system="test system",
        memory_blocks=memory_blocks,
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        tool_ids=[print_tool.id],
        include_default_source=True,
        include_base_tools=False,
        tags=["test_tag"],
    )

    # Create the agent
    created_agent = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )

    # Test legacy default include_relationships
    agent_state = await server.agent_manager.get_agent_by_id_async(
        agent_id=created_agent.id,
        actor=default_user,
    )
    assert agent_state.blocks
    assert agent_state.sources
    assert agent_state.tags
    assert agent_state.tools

    # Test include_relationships override
    agent_state = await server.agent_manager.get_agent_by_id_async(
        agent_id=created_agent.id,
        actor=default_user,
        include_relationships=[],
    )
    assert not agent_state.blocks
    assert not agent_state.sources
    assert not agent_state.tags
    assert not agent_state.tools

    # Test include_relationships override with specific relationships
    # Note: tags are always loaded alongside memory (needed for git_enabled)
    agent_state = await server.agent_manager.get_agent_by_id_async(
        agent_id=created_agent.id,
        actor=default_user,
        include_relationships=["memory", "sources"],
    )
    assert agent_state.blocks
    assert agent_state.sources
    assert agent_state.tags  # tags loaded with memory for git_enabled
    assert not agent_state.tools

    # Test include override with specific relationships
    agent_state = await server.agent_manager.get_agent_by_id_async(
        agent_id=created_agent.id,
        actor=default_user,
        include_relationships=[],
        include=["agent.blocks", "agent.sources"],
    )
    assert agent_state.blocks
    assert agent_state.sources
    assert agent_state.tags  # tags loaded with blocks for git_enabled
    assert not agent_state.tools


async def test_create_template_agent_with_files_from_sources(server: SyncServer, default_user, print_tool, default_block):
    """Test that agents created from templates properly attach files from their sources"""
    from letta.schemas.file import FileMetadata as PydanticFileMetadata

    memory_blocks = [CreateBlock(label="human", value="TestUser"), CreateBlock(label="persona", value="I am a test assistant")]

    # Create a source with files
    source = await server.source_manager.create_source(
        source=PydanticSource(
            name="test_template_source",
            embedding_config=EmbeddingConfig.default_config(provider="openai"),
        ),
        actor=default_user,
    )

    # Create files in the source
    file1_metadata = PydanticFileMetadata(
        file_name="template_file_1.txt",
        organization_id=default_user.organization_id,
        source_id=source.id,
    )
    await server.file_manager.create_file(file_metadata=file1_metadata, actor=default_user, text="content for file 1")

    file2_metadata = PydanticFileMetadata(
        file_name="template_file_2.txt",
        organization_id=default_user.organization_id,
        source_id=source.id,
    )
    await server.file_manager.create_file(file_metadata=file2_metadata, actor=default_user, text="content for file 2")

    # Create agent using InternalTemplateAgentCreate with the source
    create_agent_request = InternalTemplateAgentCreate(
        name="test_template_agent_with_files",
        agent_type="memgpt_v2_agent",
        system="test system",
        memory_blocks=memory_blocks,
        llm_config=LLMConfig.default_config("gpt-4o-mini"),
        embedding_config=EmbeddingConfig.default_config(provider="openai"),
        block_ids=[default_block.id],
        tool_ids=[print_tool.id],
        source_ids=[source.id],  # Attach the source with files
        include_base_tools=False,
        base_template_id="base_template_123",
        template_id="template_456",
        deployment_id="deployment_789",
        entity_id="entity_012",
    )

    # Create the agent
    created_agent = await server.agent_manager.create_agent_async(
        create_agent_request,
        actor=default_user,
    )

    # Verify agent was created
    assert created_agent is not None
    assert created_agent.name == "test_template_agent_with_files"

    # Verify that the source is attached
    attached_sources = await server.agent_manager.list_attached_sources_async(agent_id=created_agent.id, actor=default_user)
    assert len(attached_sources) == 1
    assert attached_sources[0].id == source.id

    # Verify that files from the source are attached to the agent
    attached_files = await server.file_agent_manager.list_files_for_agent(
        created_agent.id, per_file_view_window_char_limit=created_agent.per_file_view_window_char_limit, actor=default_user
    )

    # Should have both files attached
    assert len(attached_files) == 2
    attached_file_names = {f.file_name for f in attached_files}
    assert "template_file_1.txt" in attached_file_names
    assert "template_file_2.txt" in attached_file_names

    # Verify files are properly linked to the source
    for attached_file in attached_files:
        assert attached_file.source_id == source.id

    # Clean up
    await server.agent_manager.delete_agent_async(created_agent.id, default_user)
    await server.source_manager.delete_source(source.id, default_user)
