# agent_loop.py - Raw OpenAI-compatible agent loop: run_raw_agent, get_state, config, normalize. Used by aiagent.
from __future__ import annotations

import contextvars
import hashlib
import json
import logging
import os
import random
import re
import threading
import time
import unicodedata
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from threading import Timer
from typing import Any, Callable

import httpx
from openai import OpenAI
from persistence import (
    MUGI_DIR,
    get_proactive_nudge_turns,
    set_proactive_nudge_turns,
    set_current_agent_thread_id,
    get_current_agent_thread_id,
)

from core import reload_config_if_changed, get_workspace_path, get_agent_display_name, load_personality_prompt, evolution_enabled
from agent_loop_xml_tracking import record_xml_salvage_round
from agent_loop_guard import (
    _GUARD_STREAK_COOL_CAP,
    _GUARD_STREAK_COOLER_CAP,
    effective_llm_temperature_for_guard_streak,
)
from agent_loop_api_messages import (
    _sanitize_messages_for_api,
    _prepare_messages_for_vision_api,
)
from utils.agent_run_context import (
    AgentRunContext,
    reset_current_run_context,
    set_current_run_context,
)
from drive_engine import (
    BACKGROUND_HEARTBEAT_THREAD_ID,
    DRIVE_ACTION_THREAD_PREFIX,
    HEARTBEAT_THREAD_PREFIX,
)
from utils.llm_output_sanitize import (
    strip_reasoning_preamble_leak as _strip_reasoning_preamble_leak,
    strip_untagged_cot as _strip_untagged_cot,
    strip_untagged_cot_with_meta as _strip_untagged_cot_with_meta,
)
from utils.assistant_visibility_placeholders import (
    PLACEHOLDER_NO_VISIBLE_REPLY,
    PLACEHOLDER_TOOL_FORMAT_GLITCH,
)

_log = logging.getLogger(__name__)


def _effective_llm_temperature_for_guard_streak(base: float, streak: int) -> float:
    """Compatibility shim for legacy imports/tests."""
    return effective_llm_temperature_for_guard_streak(base, streak, get_llm_temperature())

from tools import (
    get_tools,
    get_tools_tiered,
    choose_ocr_method,
    _ocr_runner_complex,
    emit_plugin_stream_event,
    is_tool_realtime,
)
from tools.conversation_trace import (
    is_trace_enabled,
    emit_run_start,
    emit_run_end,
    emit_turn_start,
    emit_llm_request,
    emit_llm_response,
    emit_tool_call,
    emit_validation_guard,
    emit_turn_end,
    emit_guardrail_event,
)
from utils.path_safety import normalize_path_spaces, safe_resolve_path
from agent_research import (
    _research_report_gate_block_reason,
    find_latest_scratch_path,
    estimate_tokens,
    write_scratch_from_findings,
    trim_old_large_tool_results,
    RESEARCH_INDICATOR_TOOLS,
    RESEARCH_DEPTH_TOOLS,
    RESEARCH_SOFT_DEPTH_OVERFLOW,
    SAVE_RESEARCH_REMINDER_ROUND,
    CTX_SMALL_THRESHOLD_TOKENS,
    MIN_SCRATCH_APPENDS_FOR_REPORT,
    MIN_SCRATCH_FILE_SIZE,
)
from agent_loop_prompt import (  # noqa: F401
    build_main_system_prompt_segments, build_system_prompt,
    PERSONALITY_FILES_REMINDER, _ONBOARDING_WHEN_IDENTITY_SET, _ONBOARDING_BLOCK_PATTERN,
)
from agent_loop_config import (  # noqa: F401
    _normalize_backend_url, get_backend_base_url, _ensure_local_mlx_server_ready,
    get_llmster_model, _get_mlx_active_adapter, _read_pref,
    get_llm_context_length, get_research_context_params, get_research_limits,
    get_agent_loop_limits,
    DEFAULT_AGENT_MAX_TOOL_ROUNDS,
    get_llm_temperature, get_llm_max_tokens, get_vision_image_format, get_memory_policy_prefs,
    CHATS_DB_DIR, APP_PREFS_FILENAME, DEFAULT_BACKEND_BASE_URL,
    get_mlx_openai_model_id,
    get_mlx_native_tool_probe_ok,
    probe_openai_compatible_first_model_id,
)
from agent_loop_tools import (  # noqa: F401
    _tool_to_openai_schema, set_plugin_manager, get_openai_tools,
    _get_plugin_manager,
)
from research_run_state import (
    ResearchRunState,
    empty_state as _empty_research_state,
    load_state_for_scratch as _load_research_state_for_scratch,
    bind_state_to_scratch as _bind_research_state_to_scratch,
    save_state as _save_research_state,
    merge_url as _merge_research_state_url,
    parse_urls_from_deep_search_output as _parse_urls_from_deep_search_output,
    parse_credibility_from_analysis as _parse_credibility_from_analysis,
)
from agent_tool_format import (
    TOOL_INVOCATION_GUIDANCE,
    TOOL_INVOCATION_GUIDANCE_SHORT,
    ACCEPTED_XML_FORMATS_BULLETS,
    GUARD_TOOL_CALL_MINIMAL,
    GUARD_TOOL_CALL_SUFFIX,
)
from utils.transcript_roles import (
    extract_inner_tool_response_text,
    is_tool_result_carrier_message,
    is_real_user_message,
    tool_result_text_for_trim,
)
from utils.memory_retrieval_tier import (
    MemoryRetrievalTier,
    RetrievalTierSnapshot,
    choose_retrieval_tier_from_recent_user_texts,
    looks_like_synthetic_user_message,
    set_current_retrieval_tier,
    reset_current_retrieval_tier,
    get_current_retrieval_tier,
    apply_search_sessions_tier_cap,
    tier_skills_prompt_suffix,
)
from utils.mandatory_tool_guard import mandatory_tool_retry_needed
from utils.llm_token_normalize import normalize_kt_task_id_token
from utils.tool_evidence import (
    NO_TOOL_EVIDENCE_NUDGE,
    lacks_usable_evidence,
    should_inject_no_evidence_nudge,
)

# Tool names that show "Research Mode — " prefix and trigger input bar blue tint in the app
_RESEARCH_MODE_TOOL_NAMES = frozenset((
    "deep_web_search", "read_and_analyze_page", "analyze_multiple_sources",
    "append_research_scratch", "save_research_findings", "generate_final_research_report",
    "suggest_follow_up_queries", "reflect_research_state", "plan_research_subquestions",
))

# Subset available even without research mode — better search/read that agents
# always reach for.  The remaining pipeline tools (scratch, report, synthesis)
# stay gated behind research_mode.
_ALWAYS_AVAILABLE_RESEARCH_TOOLS = frozenset((
    "deep_web_search", "read_and_analyze_page",
))

_RESEARCH_PIPELINE_ONLY_TOOLS = _RESEARCH_MODE_TOOL_NAMES - _ALWAYS_AVAILABLE_RESEARCH_TOOLS

# ---------------------------------------------------------------------------
# Extracted modules (re-exported for stable `from agent_loop import …` / tests).
# ---------------------------------------------------------------------------
from agent_loop_tool_repair import (
    _recovery_path_kv_re,
    _extract_recovery_path_candidates,
    _validate_recovered_path_candidate,
    _score_path_candidate_for_recovery,
    _repair_artifact_path_values_in_params,
    _repair_artifact_path_values_in_tool_args_dict,
    _escape_control_chars_in_json_strings,
    _json_loads_lenient,
    _extract_balanced_json_value,
    _quote_json_value,
    _salvage_json_tool_call,
    _collapse_spaced_uuids_in_jsonish_text,
    _collapse_spaced_urls_in_jsonish_quoted_strings,
    _preclean_gemma_tool_call_brace_args,
    _collapse_http_url_at,
    _repair_embedded_http_urls,
    _repair_arg_whitespace,
    _finalize_xml_parsed_arguments_string,
    _preflight_repair_arg_whitespace,
    _preflight_filter_path_artifact_tool_calls,
    _attempt_recover_query_args,
    _attempt_recover_read_tool_args,
    _try_recover_all_poisoned_path_calls,
)
from agent_loop_tool_parse import (
    _TOOL_CALL_BLOCK_RE,
    _TOOL_CALL_TAG_RE,
    _TOOL_CALL_OPEN_TAGS,
    _TOOL_CALL_CLOSE_TAGS,
    _PARAM_CLOSE_RE,
    _FUNC_CLOSE_RE,
    _extract_xml_params_robust,
    _parse_xml_tool_call_block,
    _extract_unclosed_tool_call_block,
    _parse_xml_tool_calls,
    _native_xml_tool_call_key,
    narrow_merge_native_xml_tool_calls,
    union_merge_native_xml_tool_calls,
    _MAX_UNION_NATIVE_XML_TOOL_CALLS,
    _xml_calls_delegate_task_only,
    _format_delegate_task_native_repair_prompt,
    _PROMISED_DELEGATE_HINT_RE,
    _delegate_task_in_assistant_message,
    _delegate_task_called_since_last_user,
    _last_tool_message_content,
    _last_tool_suggests_mesh_handshake_wait,
    _promised_delegate_without_tool,
    _format_promised_delegate_retry_prompt,
    _strip_xml_tool_calls,
)
from agent_loop_stream import (
    _LLM_RETRY_ATTEMPTS,
    _LLM_RETRY_BASE_DELAY,
    _LLM_RETRY_MAX_DELAY,
    _llm_bucket_acquire,
    StreamIdleTimeout,
    _scaled_idle_timeout,
    _iter_stream_with_timeout,
    _is_retryable_llm_error,
    _is_retryable_stream_error,
    _is_kraken_error,
    normalize_stream_text,
    _log_ts,
)
from agent_loop_assistant_text import (
    _THINK_TAG_RE,
    _THINK_UNCLOSED_RE,
    _THINK_OPEN_TAGS,
    _THINK_CLOSE_TAGS,
    _strip_think_tags,
    _user_requested_non_english,
    _strip_mixed_script_glitches,
    _finalize_assistant_text_for_user,
    _assistant_history_content_after_tool_extraction,
    _assistant_final_text_looks_like_tool_dump,
    _ensure_nonempty_assistant_reply,
    _extract_conversation_topics,
    _estimate_response_confidence,
)
from agent_loop_guardrails import (
    _NOOP_PROMISE_RE,
    _NOOP_RETRY_PHRASE_RE,
    _NOOP_ACTION_START_RE,
    _NOOP_ACTION_VERB_RE,
    _NOOP_NON_ACTION_PHRASE_RE,
    _NOOP_PLACEHOLDER_SUFFIX_RE,
    _NOOP_PROPER_TAIL_RE,
    _TEXT_ONLY_SEARCH_SESSIONS_SHIM_RE,
    _TOOL_ERROR_HINT_RE,
    _TOOL_FAILURE_PREFIXES,
    _tool_result_indicates_failure,
    _tool_failure_kind,
    ROUND_OUTCOME_OK,
    ROUND_OUTCOME_UNKNOWN,
    ROUND_OUTCOME_POLICY,
    _KANBAN_HANDOFF_MIN_RAW_CHARS,
    _classify_tool_result,
    _update_consecutive_failure_streaks,
    _update_consecutive_hard_tool_error_streak,
    _kanban_handoff_retry_reason,
    _is_recoverable_all_failed_round,
    _format_all_tools_failed_recovery_message,
    _multi_step_user_message,
    _is_heartbeat_background_thread,
    _heartbeat_exploratory_miss_ok,
    _has_recent_tool_error,
    _last_user_text_goal,
    _recent_tool_digest_for_completion_critic,
    _recent_policy_blocked_shell_execute,
    _is_noop_action_promise,
    _is_text_only_search_sessions_shim,
    _required_tool_fields,
    _missing_required_tool_args,
    _preflight_drop_tool_calls_missing_required,
    _is_read_chunk_only_turn,
    _read_chunk_turn_signatures,
    _should_append_invalid_required_retry,
    _format_invalid_required_retry_prompt,
    _derive_topic_from_content,
    _auto_fill_scratch_args,
    _recover_scratch_content_from_messages,
    _infer_scratch_topic_from_context,
    _format_path_artifact_retry_prompt,
    _cap_tool_diag_text,
    _diagnose_single_tool_call_block,
    _diagnose_tool_call_parse_failure,
    _format_malformed_tool_call_retry_prompt,
    _format_noop_promise_retry_prompt,
    _format_shell_policy_recovery_prompt,
    _format_text_only_search_sessions_shim_retry_prompt,
    _format_task_plan_continuation_prompt,
    _task_plan_enforcement_enabled,
    _completion_critic_env_enabled,
    _TOOL_CALL_LOOP_DEFAULT_THRESHOLD,
    _TOOL_CALL_LOOP_RECOVERY_THRESHOLD,
    _TOOL_CALL_LOOP_HARD_STOP_THRESHOLD,
    _TOOL_CALL_LOOP_EXEMPT_TOOLS,
    _coarse_tool_call_signature,
    _tool_call_loop_signature,
    _update_tool_call_loop_counts,
    _select_tool_call_loop_recovery_trips,
    _select_tool_call_loop_hard_stop_trips,
    _select_kanban_observe_loop_hard_stop_trips,
    _select_kanban_verify_loop_hard_stop_trips,
    _format_tool_call_loop_nudge,
    _format_tool_call_loop_recovery,
    _format_tool_call_loop_hard_stop_message,
    _format_kanban_observe_loop_salvage_prompt,
    synthesize_kanban_loop_salvage_handoff,
    kanban_observe_loop_hard_stop_threshold,
    _looks_like_future_action_promise,
)
from agent_loop_trim_context import (
    CHATS_DB_FILENAME,
    CHECKPOINT_DB_FILENAME,
    MAX_TOOL_ROUNDS,
    REPETITION_WINDOW,
    MAX_TOOL_RESULT_CHARS,
    MAX_READ_FILE_CHUNK_RESULT_CHARS,
    PROACTIVE_NUDGE_MIN_TURNS,
    PROACTIVE_NUDGE_PROBABILITY,
    PROACTIVE_NUDGE_RECENT_ROUNDS,
    _READ_FILE_CHUNK_CONTEXT_KEEP,
    _EMERGENCY_KEEP_TAIL,
    _save_round_checkpoint,
    _load_round_checkpoint,
    _clear_round_checkpoint,
    _get_openai_client,
    get_state,
    _Msg,
    _State,
    _maybe_save_large_tool_result,
    _normalize_search_query,
    _search_queries_similar,
    _is_nonactionable_search_result,
    _get_recent_similar_search_result,
    _is_real_user_query,
    _emergency_trim_messages,
    _read_working_buffer_for_inject,
    _trim_old_read_file_chunk_results,
    _content_chars,
    _delta_tc_field,
    _resolve_stream_tool_call_index,
    _agent_was_proactive_recently,
    _is_synthetic_startup_init,
    _latest_real_user_text,
    _has_explicit_recall_intent,
    _escape_untrusted_system_context,
    _thread_disallows_recall_preflight,
    _looks_like_unsupported_no_recall_claim,
    _looks_like_plain_conversational_turn,
)



def run_raw_agent(
    messages: list[dict[str, Any]],
    thread_id: str,
    config: dict[str, Any] | None,
    on_chunk: Callable[[str], None],
    on_log: Callable[[str, str], None] | None = None,
    on_error: Callable[[str], None] | None = None,
    on_finished: Callable[[str], None] | None = None,
    on_status: Callable[[str], None] | None = None,
    on_tool_event: Callable[[dict[str, Any]], None] | None = None,
    on_agent_message: Callable[[str, bool], None] | None = None,
    on_new_assistant_content: Callable[[], None] | None = None,
    on_replace_last_content: Callable[[str], None] | None = None,
    _last_output_path_ref: list[str] | None = None,
    _memory_turn_summary_ref: list[Any] | None = None,
    _knowledge_turn_summary_ref: list[Any] | None = None,
    stop_check: Callable[[], bool] | None = None,
    research_mode: bool = False,
    has_media_attachment: bool = False,
    include_cli: bool = False,
    subagent_mode: bool = False,
    model_override: str | None = None,
    base_url_override: str | None = None,
    api_key_override: str | None = None,
    tools_override: list[Any] | None = None,
    subagent_personality_hint: str | None = None,
    subagent_system_override: str | None = None,
    kanban_worker_mode: bool = False,
    temperature: float | None = None,
    max_tokens: int | None = None,
    stop: list[str] | str | None = None,
    response_format: dict[str, Any] | None = None,
    policy_context: Any | None = None,
    iteration_budget: Any | None = None,
    quick_panel: bool = False,
    dashboard_surface: bool = False,
    force_plain_chat: bool = False,
) -> str | None:
    """Run the agent with raw OpenAI-compatible API: streaming chat + function calling loop.

    Returns the final assistant text from ``execute_raw_agent`` when the run completes (same value
    passed to ``on_finished``). Callers such as ``drive_engine`` / ``curiosity_engine`` already read
    this return value alongside streamed chunks.
    """
    # Kernel cutover (docs/kernel_spec.md §8): every surface (chat, subagent,
    # kanban-worker, quick-panel, dashboard) can route through the extracted
    # kernel behind the [kernel] enabled flag. Default OFF: the Phase 5 cutover
    # was reverted after the soak found the kernel's content salvage
    # (core/model.py) misses non-Hermes tool-call formats (Gemma channel-style
    # call:name{args}) that legacy agent_loop_tool_parse.py handles. Structured-
    # output (response_format) runs always stay on legacy. Any wiring failure
    # falls back to legacy so the flag is instantly revertible.
    try:
        from core.config import get_kernel_config

        _kernel_on = bool(get_kernel_config().get("enabled"))
    except Exception:
        _kernel_on = False
    if _kernel_on:
        _chat_eligible = (
            not subagent_mode
            and not research_mode
            and not kanban_worker_mode
            and not force_plain_chat
            and tools_override is None
            and subagent_system_override is None
            and response_format is None
        )
        if _chat_eligible:
            # Phase 4c: foreground surfaces get their own kernel profile —
            # quick-panel and dashboard add their prompt suffix + merged tools;
            # everything else is plain chat. All three share the run_raw_agent
            # contract, so the kwargs and the single legacy fallback are common.
            try:
                from utils import kernel_wiring as _kw

                if quick_panel:
                    _kernel_foreground = _kw.run_kernel_quick_panel
                elif dashboard_surface:
                    _kernel_foreground = _kw.run_kernel_dashboard
                else:
                    _kernel_foreground = _kw.run_kernel_chat

                return _kernel_foreground(
                    messages,
                    thread_id,
                    config,
                    on_chunk,
                    on_log=on_log,
                    on_error=on_error,
                    on_finished=on_finished,
                    on_status=on_status,
                    on_tool_event=on_tool_event,
                    stop_check=stop_check,
                    model_override=model_override,
                    base_url_override=base_url_override,
                    api_key_override=api_key_override,
                    temperature=temperature,
                    max_tokens=max_tokens,
                    quick_panel=quick_panel,
                    dashboard_surface=dashboard_surface,
                )
            except Exception:
                import logging

                logging.getLogger(__name__).warning(
                    "kernel foreground path failed; falling back to legacy execute_raw_agent",
                    exc_info=True,
                )
        elif subagent_mode and not kanban_worker_mode and response_format is None:
            # Phase 4a: subagent profile on the kernel loop. Note (blast radius):
            # with the flag on this routes every non-kanban subagent_mode run —
            # including background heartbeat / curiosity / drive / mesh subagents.
            try:
                from utils.kernel_wiring import run_kernel_subagent

                return run_kernel_subagent(
                    messages,
                    thread_id,
                    config,
                    on_chunk,
                    on_log=on_log,
                    on_error=on_error,
                    on_finished=on_finished,
                    on_status=on_status,
                    on_tool_event=on_tool_event,
                    stop_check=stop_check,
                    model_override=model_override,
                    base_url_override=base_url_override,
                    api_key_override=api_key_override,
                    temperature=temperature,
                    max_tokens=max_tokens,
                    tools_override=tools_override,
                    subagent_personality_hint=subagent_personality_hint,
                    subagent_system_override=subagent_system_override,
                    iteration_budget=iteration_budget,
                )
            except Exception:
                import logging

                logging.getLogger(__name__).warning(
                    "kernel subagent path failed; falling back to legacy execute_raw_agent",
                    exc_info=True,
                )
        elif subagent_mode and kanban_worker_mode and response_format is None:
            # Phase 4b: kanban-worker profile on the kernel loop. Honors the
            # dispatcher's policy_context (kanban_worker / kanban_allow_shell)
            # and the bound claim token; the dispatcher still owns completion.
            try:
                from utils.kernel_wiring import run_kernel_kanban_worker

                return run_kernel_kanban_worker(
                    messages,
                    thread_id,
                    config,
                    on_chunk,
                    on_log=on_log,
                    on_error=on_error,
                    on_finished=on_finished,
                    on_status=on_status,
                    on_tool_event=on_tool_event,
                    stop_check=stop_check,
                    model_override=model_override,
                    base_url_override=base_url_override,
                    api_key_override=api_key_override,
                    temperature=temperature,
                    max_tokens=max_tokens,
                    tools_override=tools_override,
                    subagent_personality_hint=subagent_personality_hint,
                    subagent_system_override=subagent_system_override,
                    iteration_budget=iteration_budget,
                    policy_context=policy_context,
                )
            except Exception:
                import logging

                logging.getLogger(__name__).warning(
                    "kernel kanban-worker path failed; falling back to legacy execute_raw_agent",
                    exc_info=True,
                )

    from agent_loop_run import execute_raw_agent

    from utils.memory_turn_trace import memory_turn_trace_begin, memory_turn_trace_finalize
    from utils.knowledge_turn_trace import knowledge_turn_trace_begin, knowledge_turn_trace_finalize

    memory_turn_trace_begin(thread_id, _memory_turn_summary_ref)
    knowledge_turn_trace_begin(thread_id, _knowledge_turn_summary_ref)
    finished = on_finished
    if on_finished is not None and (
        _memory_turn_summary_ref is not None or _knowledge_turn_summary_ref is not None
    ):
        cb = on_finished

        def finished(text: str) -> None:
            memory_turn_trace_finalize(_memory_turn_summary_ref)
            knowledge_turn_trace_finalize(_knowledge_turn_summary_ref)
            cb(text)

    try:
        return execute_raw_agent(
            messages,
            thread_id,
            config,
            on_chunk,
            on_log=on_log,
            on_error=on_error,
            on_finished=finished,
            on_status=on_status,
            on_tool_event=on_tool_event,
            on_agent_message=on_agent_message,
            on_new_assistant_content=on_new_assistant_content,
            on_replace_last_content=on_replace_last_content,
            _last_output_path_ref=_last_output_path_ref,
            stop_check=stop_check,
            research_mode=research_mode,
            has_media_attachment=has_media_attachment,
            include_cli=include_cli,
            subagent_mode=subagent_mode,
            model_override=model_override,
            base_url_override=base_url_override,
            api_key_override=api_key_override,
            tools_override=tools_override,
            subagent_personality_hint=subagent_personality_hint,
            subagent_system_override=subagent_system_override,
            kanban_worker_mode=kanban_worker_mode,
            temperature=temperature,
            max_tokens=max_tokens,
            stop=stop,
            response_format=response_format,
            policy_context=policy_context,
            iteration_budget=iteration_budget,
            quick_panel=quick_panel,
            dashboard_surface=dashboard_surface,
            force_plain_chat=force_plain_chat,
        )
    finally:
        memory_turn_trace_finalize(_memory_turn_summary_ref)
        knowledge_turn_trace_finalize(_knowledge_turn_summary_ref)

# Legacy path; raw agent uses thread JSON store (thread_<id>.json).
_checkpoint_path = os.path.join(CHATS_DB_DIR, CHECKPOINT_DB_FILENAME)

# Backward compatibility for legacy app (legacy/app_pyobjc2.main()): raw agent does not use LangGraph checkpointer
_SqliteSaver = None


class _MemorySaverStub:
    """Dummy for app main() when _SqliteSaver is None; not used by raw agent."""


MemorySaver = _MemorySaverStub


def _create_agent(checkpointer, base_url=None):
    """No-op for backward compatibility; returns current agent stub (raw agent does not use LangGraph)."""
    return agent


# Stub for app: loadConversationForThread_ and conversationMessageCountForThread_ use agent.get_state(config)
class _AgentStub:
    @staticmethod
    def get_state(config):
        return get_state(config)


agent = _AgentStub()

# Stable facade for mugi_backend, aiagent, tools, and tests (`from agent_loop import …`).
__all__ = [
    "APP_PREFS_FILENAME",
    "CHATS_DB_DIR",
    "CHATS_DB_FILENAME",
    "CHECKPOINT_DB_FILENAME",
    "DEFAULT_BACKEND_BASE_URL",
    "MemorySaver",
    "_create_agent",
    "_ensure_local_mlx_server_ready",
    "_get_openai_client",
    "_normalize_backend_url",
    "_parse_xml_tool_calls",
    "agent",
    "get_backend_base_url",
    "get_llm_context_length",
    "get_llm_max_tokens",
    "get_llmster_model",
    "get_llm_temperature",
    "get_openai_tools",
    "get_state",
    "get_vision_image_format",
    "normalize_stream_text",
    "probe_openai_compatible_first_model_id",
    "run_raw_agent",
    "set_plugin_manager",
]

