diff --git a/.github/workflows/loongsuite_lint_0.yml b/.github/workflows/loongsuite_lint_0.yml index 4ca6f1b60..4a4e81c26 100644 --- a/.github/workflows/loongsuite_lint_0.yml +++ b/.github/workflows/loongsuite_lint_0.yml @@ -84,7 +84,7 @@ jobs: shell: bash env: LOONGSUITE_ALL_JOBS: >- - [{"name": "lint-loongsuite-instrumentation-agentscope", "package": "loongsuite-instrumentation-agentscope", "tox_env": "lint-loongsuite-instrumentation-agentscope", "ui_name": "loongsuite-instrumentation-agentscope"}, {"name": "lint-loongsuite-instrumentation-dashscope", "package": "loongsuite-instrumentation-dashscope", "tox_env": "lint-loongsuite-instrumentation-dashscope", "ui_name": "loongsuite-instrumentation-dashscope"}, {"name": "lint-loongsuite-instrumentation-claude-agent-sdk", "package": "loongsuite-instrumentation-claude-agent-sdk", "tox_env": "lint-loongsuite-instrumentation-claude-agent-sdk", "ui_name": "loongsuite-instrumentation-claude-agent-sdk"}, {"name": "lint-loongsuite-instrumentation-google-adk", "package": "loongsuite-instrumentation-google-adk", "tox_env": "lint-loongsuite-instrumentation-google-adk", "ui_name": "loongsuite-instrumentation-google-adk"}, {"name": "lint-loongsuite-instrumentation-agno", "package": "loongsuite-instrumentation-agno", "tox_env": "lint-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno"}, {"name": "lint-loongsuite-instrumentation-langchain", "package": "loongsuite-instrumentation-langchain", "tox_env": "lint-loongsuite-instrumentation-langchain", "ui_name": "loongsuite-instrumentation-langchain"}, {"name": "lint-loongsuite-instrumentation-langgraph", "package": "loongsuite-instrumentation-langgraph", "tox_env": "lint-loongsuite-instrumentation-langgraph", "ui_name": "loongsuite-instrumentation-langgraph"}, {"name": "lint-loongsuite-instrumentation-autogen", "package": "loongsuite-instrumentation-autogen", "tox_env": "lint-loongsuite-instrumentation-autogen", "ui_name": "loongsuite-instrumentation-autogen"}, {"name": "lint-loongsuite-instrumentation-deepagents", "package": "loongsuite-instrumentation-deepagents", "tox_env": "lint-loongsuite-instrumentation-deepagents", "ui_name": "loongsuite-instrumentation-deepagents"}, {"name": "lint-loongsuite-instrumentation-microsoft-agent-framework", "package": "loongsuite-instrumentation-microsoft-agent-framework", "tox_env": "lint-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework"}, {"name": "lint-loongsuite-instrumentation-qwen-agent", "package": "loongsuite-instrumentation-qwen-agent", "tox_env": "lint-loongsuite-instrumentation-qwen-agent", "ui_name": "loongsuite-instrumentation-qwen-agent"}, {"name": "lint-loongsuite-instrumentation-hermes-agent", "package": "loongsuite-instrumentation-hermes-agent", "tox_env": "lint-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent"}, {"name": "lint-loongsuite-instrumentation-mem0", "package": "loongsuite-instrumentation-mem0", "tox_env": "lint-loongsuite-instrumentation-mem0", "ui_name": "loongsuite-instrumentation-mem0"}, {"name": "lint-util-genai", "package": "util-genai", "tox_env": "lint-util-genai", "ui_name": "util-genai"}, {"name": "lint-loongsuite-instrumentation-litellm", "package": "loongsuite-instrumentation-litellm", "tox_env": "lint-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm"}, {"name": "lint-loongsuite-instrumentation-crewai", "package": "loongsuite-instrumentation-crewai", "tox_env": "lint-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai"}, {"name": "lint-loongsuite-instrumentation-qwenpaw", "package": "loongsuite-instrumentation-qwenpaw", "tox_env": "lint-loongsuite-instrumentation-qwenpaw", "ui_name": "loongsuite-instrumentation-qwenpaw"}, {"name": "lint-loongsuite-instrumentation-algotune", "package": "loongsuite-instrumentation-algotune", "tox_env": "lint-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune"}, {"name": "lint-loongsuite-instrumentation-bfclv4", "package": "loongsuite-instrumentation-bfclv4", "tox_env": "lint-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4"}, {"name": "lint-loongsuite-instrumentation-claw-eval", "package": "loongsuite-instrumentation-claw-eval", "tox_env": "lint-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval"}, {"name": "lint-loongsuite-instrumentation-minisweagent", "package": "loongsuite-instrumentation-minisweagent", "tox_env": "lint-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent"}, {"name": "lint-loongsuite-instrumentation-openhands", "package": "loongsuite-instrumentation-openhands", "tox_env": "lint-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands"}, {"name": "lint-loongsuite-instrumentation-slop-code", "package": "loongsuite-instrumentation-slop-code", "tox_env": "lint-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code"}, {"name": "lint-loongsuite-instrumentation-terminus2", "package": "loongsuite-instrumentation-terminus2", "tox_env": "lint-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2"}, {"name": "lint-loongsuite-instrumentation-vita", "package": "loongsuite-instrumentation-vita", "tox_env": "lint-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita"}, {"name": "lint-loongsuite-instrumentation-webarena", "package": "loongsuite-instrumentation-webarena", "tox_env": "lint-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena"}, {"name": "lint-loongsuite-instrumentation-widesearch", "package": "loongsuite-instrumentation-widesearch", "tox_env": "lint-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch"}, {"name": "lint-loongsuite-instrumentation-wildtool", "package": "loongsuite-instrumentation-wildtool", "tox_env": "lint-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool"}] + [{"name": "lint-loongsuite-instrumentation-agentscope", "package": "loongsuite-instrumentation-agentscope", "tox_env": "lint-loongsuite-instrumentation-agentscope", "ui_name": "loongsuite-instrumentation-agentscope"}, {"name": "lint-loongsuite-instrumentation-dashscope", "package": "loongsuite-instrumentation-dashscope", "tox_env": "lint-loongsuite-instrumentation-dashscope", "ui_name": "loongsuite-instrumentation-dashscope"}, {"name": "lint-loongsuite-instrumentation-claude-agent-sdk", "package": "loongsuite-instrumentation-claude-agent-sdk", "tox_env": "lint-loongsuite-instrumentation-claude-agent-sdk", "ui_name": "loongsuite-instrumentation-claude-agent-sdk"}, {"name": "lint-loongsuite-instrumentation-google-adk", "package": "loongsuite-instrumentation-google-adk", "tox_env": "lint-loongsuite-instrumentation-google-adk", "ui_name": "loongsuite-instrumentation-google-adk"}, {"name": "lint-loongsuite-instrumentation-agno", "package": "loongsuite-instrumentation-agno", "tox_env": "lint-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno"}, {"name": "lint-loongsuite-instrumentation-langchain", "package": "loongsuite-instrumentation-langchain", "tox_env": "lint-loongsuite-instrumentation-langchain", "ui_name": "loongsuite-instrumentation-langchain"}, {"name": "lint-loongsuite-instrumentation-langgraph", "package": "loongsuite-instrumentation-langgraph", "tox_env": "lint-loongsuite-instrumentation-langgraph", "ui_name": "loongsuite-instrumentation-langgraph"}, {"name": "lint-loongsuite-instrumentation-autogen", "package": "loongsuite-instrumentation-autogen", "tox_env": "lint-loongsuite-instrumentation-autogen", "ui_name": "loongsuite-instrumentation-autogen"}, {"name": "lint-loongsuite-instrumentation-deepagents", "package": "loongsuite-instrumentation-deepagents", "tox_env": "lint-loongsuite-instrumentation-deepagents", "ui_name": "loongsuite-instrumentation-deepagents"}, {"name": "lint-loongsuite-instrumentation-deerflow", "package": "loongsuite-instrumentation-deerflow", "tox_env": "lint-loongsuite-instrumentation-deerflow", "ui_name": "loongsuite-instrumentation-deerflow"}, {"name": "lint-loongsuite-instrumentation-microsoft-agent-framework", "package": "loongsuite-instrumentation-microsoft-agent-framework", "tox_env": "lint-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework"}, {"name": "lint-loongsuite-instrumentation-qwen-agent", "package": "loongsuite-instrumentation-qwen-agent", "tox_env": "lint-loongsuite-instrumentation-qwen-agent", "ui_name": "loongsuite-instrumentation-qwen-agent"}, {"name": "lint-loongsuite-instrumentation-hermes-agent", "package": "loongsuite-instrumentation-hermes-agent", "tox_env": "lint-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent"}, {"name": "lint-loongsuite-instrumentation-mem0", "package": "loongsuite-instrumentation-mem0", "tox_env": "lint-loongsuite-instrumentation-mem0", "ui_name": "loongsuite-instrumentation-mem0"}, {"name": "lint-util-genai", "package": "util-genai", "tox_env": "lint-util-genai", "ui_name": "util-genai"}, {"name": "lint-loongsuite-instrumentation-litellm", "package": "loongsuite-instrumentation-litellm", "tox_env": "lint-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm"}, {"name": "lint-loongsuite-instrumentation-crewai", "package": "loongsuite-instrumentation-crewai", "tox_env": "lint-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai"}, {"name": "lint-loongsuite-instrumentation-qwenpaw", "package": "loongsuite-instrumentation-qwenpaw", "tox_env": "lint-loongsuite-instrumentation-qwenpaw", "ui_name": "loongsuite-instrumentation-qwenpaw"}, {"name": "lint-loongsuite-instrumentation-algotune", "package": "loongsuite-instrumentation-algotune", "tox_env": "lint-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune"}, {"name": "lint-loongsuite-instrumentation-bfclv4", "package": "loongsuite-instrumentation-bfclv4", "tox_env": "lint-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4"}, {"name": "lint-loongsuite-instrumentation-claw-eval", "package": "loongsuite-instrumentation-claw-eval", "tox_env": "lint-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval"}, {"name": "lint-loongsuite-instrumentation-minisweagent", "package": "loongsuite-instrumentation-minisweagent", "tox_env": "lint-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent"}, {"name": "lint-loongsuite-instrumentation-openhands", "package": "loongsuite-instrumentation-openhands", "tox_env": "lint-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands"}, {"name": "lint-loongsuite-instrumentation-slop-code", "package": "loongsuite-instrumentation-slop-code", "tox_env": "lint-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code"}, {"name": "lint-loongsuite-instrumentation-terminus2", "package": "loongsuite-instrumentation-terminus2", "tox_env": "lint-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2"}, {"name": "lint-loongsuite-instrumentation-vita", "package": "loongsuite-instrumentation-vita", "tox_env": "lint-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita"}, {"name": "lint-loongsuite-instrumentation-webarena", "package": "loongsuite-instrumentation-webarena", "tox_env": "lint-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena"}, {"name": "lint-loongsuite-instrumentation-widesearch", "package": "loongsuite-instrumentation-widesearch", "tox_env": "lint-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch"}, {"name": "lint-loongsuite-instrumentation-wildtool", "package": "loongsuite-instrumentation-wildtool", "tox_env": "lint-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool"}] LOONGSUITE_FULL: ${{ steps.detect.outputs.full }} LOONGSUITE_PACKAGES: ${{ steps.detect.outputs.packages }} run: | diff --git a/.github/workflows/loongsuite_test_0.yml b/.github/workflows/loongsuite_test_0.yml index 885c95dbb..b8f311da3 100644 --- a/.github/workflows/loongsuite_test_0.yml +++ b/.github/workflows/loongsuite_test_0.yml @@ -84,7 +84,7 @@ jobs: shell: bash env: LOONGSUITE_ALL_JOBS: >- - [{"name": "py310-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-autogen_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-autogen", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-autogen", "ui_name": "loongsuite-instrumentation-autogen 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-autogen_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-autogen", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-autogen", "ui_name": "loongsuite-instrumentation-autogen 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-autogen_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-autogen", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-autogen", "ui_name": "loongsuite-instrumentation-autogen 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-autogen_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-autogen", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-autogen", "ui_name": "loongsuite-instrumentation-autogen 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.13 ARC"}, {"name": "py39-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.9", "tox_env": "py39-test-util-genai", "ui_name": "util-genai 3.9 ARC"}, {"name": "py310-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.10", "tox_env": "py310-test-util-genai", "ui_name": "util-genai 3.10 ARC"}, {"name": "py311-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.11", "tox_env": "py311-test-util-genai", "ui_name": "util-genai 3.11 ARC"}, {"name": "py312-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.12", "tox_env": "py312-test-util-genai", "ui_name": "util-genai 3.12 ARC"}, {"name": "py313-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.13", "tox_env": "py313-test-util-genai", "ui_name": "util-genai 3.13 ARC"}, {"name": "py314-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.14", "tox_env": "py314-test-util-genai", "ui_name": "util-genai 3.14 ARC"}, {"name": "pypy3-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "pypy-3.9", "tox_env": "pypy3-test-util-genai", "ui_name": "util-genai pypy-3.9 ARC"}, {"name": "py311-test-detect-loongsuite-changes_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "detect-loongsuite-changes", "python_version": "3.11", "tox_env": "py311-test-detect-loongsuite-changes", "ui_name": "detect-loongsuite-changes 3.11 ARC"}, {"name": "py310-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.13 ARC"}, {"name": "py39-test-loongsuite-distro_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-distro", "python_version": "3.9", "tox_env": "py39-test-loongsuite-distro", "ui_name": "loongsuite-distro 3.9 ARC"}, {"name": "py310-test-loongsuite-distro_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-distro", "python_version": "3.10", "tox_env": "py310-test-loongsuite-distro", "ui_name": "loongsuite-distro 3.10 ARC"}, {"name": "py311-test-loongsuite-distro_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-distro", "python_version": "3.11", "tox_env": "py311-test-loongsuite-distro", "ui_name": "loongsuite-distro 3.11 ARC"}, {"name": "py312-test-loongsuite-distro_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-distro", "python_version": "3.12", "tox_env": "py312-test-loongsuite-distro", "ui_name": "loongsuite-distro 3.12 ARC"}, {"name": "py313-test-loongsuite-distro_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-distro", "python_version": "3.13", "tox_env": "py313-test-loongsuite-distro", "ui_name": "loongsuite-distro 3.13 ARC"}] + [{"name": "py310-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-oldest", "ui_name": "loongsuite-instrumentation-agentscope-oldest 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agentscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agentscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agentscope-latest", "ui_name": "loongsuite-instrumentation-agentscope-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-oldest", "ui_name": "loongsuite-instrumentation-dashscope-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-dashscope-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-dashscope", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-dashscope-latest", "ui_name": "loongsuite-instrumentation-dashscope-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-oldest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claude-agent-sdk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claude-agent-sdk-latest", "ui_name": "loongsuite-instrumentation-claude-agent-sdk-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-oldest", "ui_name": "loongsuite-instrumentation-google-adk-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-google-adk-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-google-adk", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-google-adk-latest", "ui_name": "loongsuite-instrumentation-google-adk-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-agno_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-agno", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-agno", "ui_name": "loongsuite-instrumentation-agno 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langchain-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-oldest", "ui_name": "loongsuite-instrumentation-langchain-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langchain-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langchain", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langchain-latest", "ui_name": "loongsuite-instrumentation-langchain-latest 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-oldest", "ui_name": "loongsuite-instrumentation-langgraph-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-langgraph-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-langgraph", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-langgraph-latest", "ui_name": "loongsuite-instrumentation-langgraph-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-autogen_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-autogen", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-autogen", "ui_name": "loongsuite-instrumentation-autogen 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-autogen_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-autogen", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-autogen", "ui_name": "loongsuite-instrumentation-autogen 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-autogen_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-autogen", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-autogen", "ui_name": "loongsuite-instrumentation-autogen 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-autogen_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-autogen", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-autogen", "ui_name": "loongsuite-instrumentation-autogen 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-deepagents-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deepagents", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-deepagents-latest", "ui_name": "loongsuite-instrumentation-deepagents-latest 3.13 ARC"}, {"name": "py312-test-loongsuite-instrumentation-deerflow.stable_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deerflow", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-deerflow.stable", "ui_name": "loongsuite-instrumentation-deerflow 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-deerflow.current_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deerflow", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-deerflow.current", "ui_name": "loongsuite-instrumentation-deerflow 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-deerflow.current_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-deerflow", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-deerflow.current", "ui_name": "loongsuite-instrumentation-deerflow 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-microsoft-agent-framework_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-microsoft-agent-framework", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-microsoft-agent-framework", "ui_name": "loongsuite-instrumentation-microsoft-agent-framework 3.13 ARC"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.9 ARC"}, {"name": "py39-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.9", "tox_env": "py39-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.9 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-oldest", "ui_name": "loongsuite-instrumentation-qwen-agent-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwen-agent-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwen-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwen-agent-latest", "ui_name": "loongsuite-instrumentation-qwen-agent-latest 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-hermes-agent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-hermes-agent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-hermes-agent", "ui_name": "loongsuite-instrumentation-hermes-agent 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-mem0-oldest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-oldest", "ui_name": "loongsuite-instrumentation-mem0-oldest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-mem0-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-mem0", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-mem0-latest", "ui_name": "loongsuite-instrumentation-mem0-latest 3.13 ARC"}, {"name": "py39-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.9", "tox_env": "py39-test-util-genai", "ui_name": "util-genai 3.9 ARC"}, {"name": "py310-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.10", "tox_env": "py310-test-util-genai", "ui_name": "util-genai 3.10 ARC"}, {"name": "py311-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.11", "tox_env": "py311-test-util-genai", "ui_name": "util-genai 3.11 ARC"}, {"name": "py312-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.12", "tox_env": "py312-test-util-genai", "ui_name": "util-genai 3.12 ARC"}, {"name": "py313-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.13", "tox_env": "py313-test-util-genai", "ui_name": "util-genai 3.13 ARC"}, {"name": "py314-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "3.14", "tox_env": "py314-test-util-genai", "ui_name": "util-genai 3.14 ARC"}, {"name": "pypy3-test-util-genai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "util-genai", "python_version": "pypy-3.9", "tox_env": "pypy3-test-util-genai", "ui_name": "util-genai pypy-3.9 ARC"}, {"name": "py311-test-detect-loongsuite-changes_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "detect-loongsuite-changes", "python_version": "3.11", "tox_env": "py311-test-detect-loongsuite-changes", "ui_name": "detect-loongsuite-changes 3.11 ARC"}, {"name": "py310-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-litellm_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-litellm", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-litellm", "ui_name": "loongsuite-instrumentation-litellm 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-crewai_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-crewai", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-crewai", "ui_name": "loongsuite-instrumentation-crewai 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.10 ARC"}, {"name": "py310-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.11 ARC"}, {"name": "py311-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.12 ARC"}, {"name": "py312-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-latest_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-latest", "ui_name": "loongsuite-instrumentation-qwenpaw-latest 3.13 ARC"}, {"name": "py313-test-loongsuite-instrumentation-qwenpaw-legacy_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-qwenpaw", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-qwenpaw-legacy", "ui_name": "loongsuite-instrumentation-qwenpaw-legacy 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-algotune_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-algotune", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-algotune", "ui_name": "loongsuite-instrumentation-algotune 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-bfclv4_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-bfclv4", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-bfclv4", "ui_name": "loongsuite-instrumentation-bfclv4 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-claw-eval_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-claw-eval", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-claw-eval", "ui_name": "loongsuite-instrumentation-claw-eval 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-minisweagent_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-minisweagent", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-minisweagent", "ui_name": "loongsuite-instrumentation-minisweagent 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-openhands_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-openhands", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-openhands", "ui_name": "loongsuite-instrumentation-openhands 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-slop-code_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-slop-code", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-slop-code", "ui_name": "loongsuite-instrumentation-slop-code 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-terminus2_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-terminus2", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-terminus2", "ui_name": "loongsuite-instrumentation-terminus2 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-vita_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-vita", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-vita", "ui_name": "loongsuite-instrumentation-vita 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-webarena_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-webarena", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-webarena", "ui_name": "loongsuite-instrumentation-webarena 3.13 ARC"}, {"name": "py311-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-widesearch_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-widesearch", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-widesearch", "ui_name": "loongsuite-instrumentation-widesearch 3.13 ARC"}, {"name": "py310-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.10", "tox_env": "py310-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.10 ARC"}, {"name": "py311-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.11", "tox_env": "py311-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.11 ARC"}, {"name": "py312-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.12", "tox_env": "py312-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.12 ARC"}, {"name": "py313-test-loongsuite-instrumentation-wildtool_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-instrumentation-wildtool", "python_version": "3.13", "tox_env": "py313-test-loongsuite-instrumentation-wildtool", "ui_name": "loongsuite-instrumentation-wildtool 3.13 ARC"}, {"name": "py39-test-loongsuite-distro_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-distro", "python_version": "3.9", "tox_env": "py39-test-loongsuite-distro", "ui_name": "loongsuite-distro 3.9 ARC"}, {"name": "py310-test-loongsuite-distro_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-distro", "python_version": "3.10", "tox_env": "py310-test-loongsuite-distro", "ui_name": "loongsuite-distro 3.10 ARC"}, {"name": "py311-test-loongsuite-distro_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-distro", "python_version": "3.11", "tox_env": "py311-test-loongsuite-distro", "ui_name": "loongsuite-distro 3.11 ARC"}, {"name": "py312-test-loongsuite-distro_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-distro", "python_version": "3.12", "tox_env": "py312-test-loongsuite-distro", "ui_name": "loongsuite-distro 3.12 ARC"}, {"name": "py313-test-loongsuite-distro_loongsuite-python-arc", "os": "loongsuite-python-arc", "package": "loongsuite-distro", "python_version": "3.13", "tox_env": "py313-test-loongsuite-distro", "ui_name": "loongsuite-distro 3.13 ARC"}] LOONGSUITE_FULL: ${{ steps.detect.outputs.full }} LOONGSUITE_PACKAGES: ${{ steps.detect.outputs.packages }} run: | diff --git a/instrumentation-loongsuite/README.md b/instrumentation-loongsuite/README.md index 397ec9c31..07bd2ab02 100644 --- a/instrumentation-loongsuite/README.md +++ b/instrumentation-loongsuite/README.md @@ -11,6 +11,7 @@ | [loongsuite-instrumentation-crewai](./loongsuite-instrumentation-crewai) | crewai >= 0.80.0 | No | development | [loongsuite-instrumentation-dashscope](./loongsuite-instrumentation-dashscope) | dashscope >= 1.0.0 | No | development | [loongsuite-instrumentation-deepagents](./loongsuite-instrumentation-deepagents) | deepagents >= 0.6.0, < 0.7.0 | No | development +| [loongsuite-instrumentation-deerflow](./loongsuite-instrumentation-deerflow) | deerflow-harness >= 2, < 3 (official source distribution) | No | development | [loongsuite-instrumentation-dify](./loongsuite-instrumentation-dify) | dify | No | development | [loongsuite-instrumentation-google-adk](./loongsuite-instrumentation-google-adk) | google-adk >= 0.1.0 | No | development | [loongsuite-instrumentation-hermes-agent](./loongsuite-instrumentation-hermes-agent) | openai >= 1.0.0 | No | development @@ -29,4 +30,4 @@ | [loongsuite-instrumentation-vita](./loongsuite-instrumentation-vita) | vita >= 0.0.1 | No | development | [loongsuite-instrumentation-webarena](./loongsuite-instrumentation-webarena) | webarena >= 0.0.1 | No | development | [loongsuite-instrumentation-widesearch](./loongsuite-instrumentation-widesearch) | widesearch >= 0.1.0 | No | development -| [loongsuite-instrumentation-wildtool](./loongsuite-instrumentation-wildtool) | openai >= 1.0.0 | No | development \ No newline at end of file +| [loongsuite-instrumentation-wildtool](./loongsuite-instrumentation-wildtool) | openai >= 1.0.0 | No | development diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md new file mode 100644 index 000000000..11561c0da --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Changed + +- Treat user cancellation, early stream closure, and DeerFlow interrupted runs + as non-error entry completions while retaining their interrupted status. +- Clarify the initial sandbox, skill, and memory observability boundaries. +- Document verified local, daemon, Docker, and embedded integration paths, + including DeerFlow's exact `uv sync` behavior. +- Keep DeerFlow trace-correlation helper failures from interrupting embedded + client streams. + +## Version 0.8.0 (2026-07-14) + +### Added + +- Initial DeerFlow 2.x graph identification and application `ENTRY` spans for + Gateway and embedded client execution. +- Propagate DeerFlow session, user, agent, assistant, run, and request trace + identities without replacing the OpenTelemetry trace id. +- Map Gateway and embedded stream success, error, timeout, interruption, and + early-close lifecycles onto a single application entry span. +- Preserve call-time OpenTelemetry and DeerFlow correlation contexts while a + synchronous embedded iterator is advanced or closed from another thread. diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md new file mode 100644 index 000000000..28c8afe35 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md @@ -0,0 +1,299 @@ +# LoongSuite DeerFlow Instrumentation + +LoongSuite instrumentation for [DeerFlow](https://github.com/bytedance/deer-flow) 2.x. + +## Features + +- Gateway and embedded-client `ENTRY` spans +- Lead-agent and subagent `AGENT` spans +- One `STEP` span for every direct model-decision node +- Existing LangChain `LLM`, `TOOL`, and supporting `CHAIN` spans +- Session, user, assistant, run, and DeerFlow correlation identities +- Streaming, cancellation, timeout, error, and concurrent-run lifecycle handling +- Coexistence with DeerFlow's LangSmith, Langfuse, and Monocle callbacks + +The expected core tree is: + +```text +ENTRY deerflow +└── AGENT lead-agent + └── STEP + ├── LLM + └── TOOL task + └── AGENT subagent: + └── STEP → LLM / TOOL +``` + +Direct invocation of a graph returned by `create_deerflow_agent()` starts at +the `AGENT` span. It does not create a synthetic application `ENTRY` span. + +## Compatibility + +- DeerFlow `>=2,<3` +- Python 3.12+ +- DeerFlow installed from the official source repository + +The similarly named `deerflow-harness` package published on PyPI is not the +official DeerFlow 2.x distribution. This instrumentation deliberately does not +offer an `instruments` extra that could install that placeholder package. + +## Choose an integration mode + +DeerFlow has several official startup modes. The instrumentation must be +installed in the Python environment used by the Gateway, not in the frontend, +nginx, sandbox, or an unrelated system Python environment. + +| DeerFlow mode | Recommended LoongSuite integration | Application source change | +| --- | --- | --- | +| Local foreground or daemon | `loongsuite-instrument` around `scripts/serve.sh` | No | +| Docker development or production | Lock `loongsuite-site-bootstrap` and the instrumentation into the backend uv project | No Python source change | +| Embedded `DeerFlowClient` or direct SDK graph | Programmatic `DeerFlowInstrumentor` | One initialization call | + +## Local foreground and daemon + +### 1. Prepare DeerFlow first + +Follow DeerFlow's official setup and let it create `backend/.venv` before +installing LoongSuite: + +```bash +git clone https://github.com/bytedance/deer-flow.git +cd deer-flow +make setup +make install +``` + +### 2. Install LoongSuite into the Gateway environment + +```bash +uv pip install \ + --python backend/.venv/bin/python \ + loongsuite-distro \ + loongsuite-instrumentation-deerflow +``` + +Install an exporter as well when it is not already present. For OTLP: + +```bash +uv pip install \ + --python backend/.venv/bin/python \ + opentelemetry-exporter-otlp +``` + +For development against a LoongSuite source checkout, follow the main +[source-install instructions](../../README.md#install-from-source-for-development) +and install this package from its local directory into the same +`backend/.venv`. + +### 3. Verify locally with the console exporter + +Run DeerFlow through the executable from `backend/.venv`: + +```bash +backend/.venv/bin/loongsuite-instrument \ + --traces_exporter console \ + --metrics_exporter none \ + --service_name deerflow \ + ./scripts/serve.sh --dev --skip-install +``` + +The instrumentation bootstrap propagates through `serve.sh` into the Gateway +Python process. The frontend and nginx processes continue to start normally. + +For a local production daemon, configure the exporter as shown below and run: + +```bash +backend/.venv/bin/loongsuite-instrument \ + ./scripts/serve.sh --prod --daemon --skip-install +``` + +> **Why `--skip-install` is required:** DeerFlow's normal `make dev` and +> `make start` paths run `uv sync`. An exact `uv sync` removes packages that +> are not recorded in DeerFlow's `pyproject.toml` and `uv.lock`. Install +> LoongSuite after the last sync and use `--skip-install`, or use the persistent +> uv-locked approach below. A subsequent `uv run` preserves the installed +> packages; a subsequent `uv sync` does not. + +## Persistent local and Docker integration + +DeerFlow Docker development runs `uv sync --all-packages` every time the +Gateway container starts. Production images also construct the backend virtual +environment from the uv lock. A host-side `pip install` therefore does not +instrument either container and an image-only ad-hoc install is not durable for +Docker development. + +For an integration that survives all official startup modes, add the +LoongSuite packages to DeerFlow's backend project: + +```bash +cd backend +uv add \ + 'loongsuite-distro[otlp]' \ + loongsuite-site-bootstrap \ + loongsuite-instrumentation-deerflow +cd .. +``` + +This intentionally updates DeerFlow's `backend/pyproject.toml` and `uv.lock`. +Commit those deployment dependency changes in your DeerFlow deployment fork so +that local development, Docker development, and production image builds use the +same packages. + +Add the bootstrap and exporter settings to DeerFlow's root `.env` file: + +```bash +LOONGSUITE_PYTHON_SITE_BOOTSTRAP=True +LOONGSUITE_PYTHON_SITE_BOOTSTRAP_LOG_SUCCESS=False + +OTEL_SERVICE_NAME=deerflow +OTEL_TRACES_EXPORTER=otlp +OTEL_METRICS_EXPORTER=otlp +OTEL_EXPORTER_OTLP_PROTOCOL=grpc +OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 +``` + +Do not commit credentials or authentication headers from exporter settings. +DeerFlow's local launcher and Docker Compose files load the root `.env`. The +site bootstrap initializes LoongSuite before DeerFlow imports its Gateway, +agent factory, or cached `create_agent` aliases. No change to +`backend/app/gateway/app.py` is required. + +You can then keep the official commands: + +```bash +# Local development +make dev + +# Docker development +make docker-start + +# Docker production +make up +``` + +For a backend outside the Compose network, replace the example collector host +with a reachable OTLP endpoint. For OTLP/HTTP use `http/protobuf` and the +corresponding endpoint instead. + +### Container verification + +Verify that the three required packages are installed in the Gateway +environment: + +```bash +docker exec deer-flow-gateway \ + /app/backend/.venv/bin/python -c \ + 'from importlib.metadata import version; print(version("loongsuite-distro")); print(version("loongsuite-site-bootstrap")); print(version("loongsuite-instrumentation-deerflow"))' +``` + +Then execute a DeerFlow request and verify that the exporter receives one +`ENTRY` and one lead `AGENT`, with one `STEP` for each model decision. A tool +loop should report model finish reasons in the order `tool_calls`, then `stop`. + +## Programmatic instrumentation + +Use programmatic setup for an embedded client or an application that already +owns its OpenTelemetry SDK initialization. Configure a tracer provider and +exporter first, then instrument before importing DeerFlow application modules: + +```python +from opentelemetry.instrumentation.deerflow import DeerFlowInstrumentor + +DeerFlowInstrumentor().instrument() + +from deerflow.client import DeerFlowClient + +client = DeerFlowClient() +``` + +Call `DeerFlowInstrumentor().uninstrument()` only during controlled teardown, +such as tests. The instrumentor composes the LoongSuite LangChain and LangGraph +instrumentations when they have not already been enabled. + +## Configuration + +### OTLP export + +```bash +export OTEL_SERVICE_NAME=deerflow +export OTEL_TRACES_EXPORTER=otlp +export OTEL_METRICS_EXPORTER=otlp +export OTEL_EXPORTER_OTLP_PROTOCOL=grpc +export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317 +``` + +Authentication headers, TLS certificates, and signal-specific endpoints use +the standard OpenTelemetry exporter environment variables. + +### Content capture + +Message content is disabled by default. To capture it explicitly: + +```bash +export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental +export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY +``` + +Supported capture modes follow the LoongSuite GenAI configuration. Keep the +default no-content mode for environments where prompts, tool arguments, or +responses can contain sensitive data. + +## Span and identity behavior + +- An existing host `ENTRY` takes precedence over DeerFlow Gateway and embedded + client entry points. +- `DeerFlowClient.chat()` reuses `stream()` and creates only one `ENTRY`. +- The embedded iterator restores its call-time context around each iterator + advance and close, including cross-thread consumption. +- `gen_ai.session.id` and `gen_ai.user.id` prefer existing baggage values over + DeerFlow identities. +- DeerFlow's request trace ID is recorded as `deerflow.trace.id`. It is a + correlation field and never replaces the W3C OpenTelemetry trace ID. +- Gateway runs record `deerflow.run.id`. The public embedded stream API does + not expose its internal run ID, so embedded spans omit that attribute. +- `task` remains a normal LangChain `TOOL`. No duplicate `TASK` span is added. +- Sandbox and skill work remains visible through downstream tool and model + calls. This package does not add separate sandbox-allocation, skill-lifecycle, + or background-memory spans. + +## Troubleshooting + +### The instrumentor silently does nothing + +Confirm that the Gateway interpreter sees the official distribution and a +supported version: + +```bash +backend/.venv/bin/python -c \ + 'import deerflow; from importlib.metadata import version; print(version("deerflow-harness"))' +``` + +The version must be `>=2,<3`. A `deerflow` module without matching +`deerflow-harness` distribution metadata is intentionally ignored. + +### Traces disappear after restarting DeerFlow + +The most common cause is a later `uv sync` removing an ad-hoc instrumentation +install. Either reinstall LoongSuite after the sync and start with +`--skip-install`, or record the packages in DeerFlow's uv project and use +`loongsuite-site-bootstrap`. + +### Docker exports no data + +Check all three boundaries separately: + +1. The packages exist inside `/app/backend/.venv`, not only on the host. +2. `LOONGSUITE_PYTHON_SITE_BOOTSTRAP=True` is present in the Gateway container. +3. The OTLP endpoint is reachable from the Gateway container; `localhost` + refers to that container, not the host or a separate collector container. + +### A direct SDK call has no `ENTRY` + +This is expected. Direct `create_deerflow_agent(...).invoke()` or `.stream()` +starts at `AGENT`. Use the Gateway or `DeerFlowClient` when an application +`ENTRY` lifecycle is required, or provide a host `ENTRY` in the embedding +application. + +## License + +Apache License 2.0 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/pyproject.toml b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/pyproject.toml new file mode 100644 index 000000000..00b67b5c0 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "loongsuite-instrumentation-deerflow" +dynamic = ["version"] +description = "LoongSuite DeerFlow Instrumentation" +readme = "README.md" +license = "Apache-2.0" +requires-python = ">=3.12" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "loongsuite-instrumentation-langchain", + "loongsuite-instrumentation-langgraph", + "opentelemetry-api ~= 1.37", + "opentelemetry-instrumentation >= 0.58b0", + "opentelemetry-semantic-conventions >= 0.58b0", + "opentelemetry-util-genai", + "packaging >= 21.0", + "wrapt >= 1.0.0, < 2.0.0", +] + +[project.entry-points.opentelemetry_instrumentor] +deerflow = "opentelemetry.instrumentation.deerflow:DeerFlowInstrumentor" + +[project.urls] +Homepage = "https://github.com/alibaba/loongsuite-python/tree/main/instrumentation-loongsuite/loongsuite-instrumentation-deerflow" +Repository = "https://github.com/alibaba/loongsuite-python" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/deerflow/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "src", + "tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/__init__.py new file mode 100644 index 000000000..ee209f666 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/__init__.py @@ -0,0 +1,209 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""LoongSuite instrumentation for ByteDance DeerFlow 2.x.""" + +from __future__ import annotations + +import importlib +import logging +from importlib.metadata import PackageNotFoundError, version +from typing import Any, Collection + +from packaging.version import InvalidVersion, Version + +from opentelemetry.instrumentation.deerflow.internal.patch import ( + instrument_deerflow, + remove_owned_langchain_alias_wrappers, + uninstrument_deerflow, +) +from opentelemetry.instrumentation.deerflow.package import _instruments +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + +__all__ = ["DeerFlowInstrumentor"] + +logger = logging.getLogger(__name__) + +_DEERFLOW_DISTRIBUTION = "deerflow-harness" +_MINIMUM_VERSION = Version("2") +_MAXIMUM_VERSION = Version("3") + + +def _is_langchain_instrumentor(instrumentor: BaseInstrumentor) -> bool: + return ( + instrumentor.__class__.__module__ + == "opentelemetry.instrumentation.langchain" + ) + + +def _deerflow_runtime_supported() -> bool: + """Validate the source-installed DeerFlow distribution at runtime.""" + try: + importlib.import_module("deerflow") + except Exception: # noqa: BLE001 + logger.debug( + "DeerFlow could not be imported; instrumentation skipped.", + exc_info=True, + ) + return False + + try: + installed_version = Version(version(_DEERFLOW_DISTRIBUTION)) + except PackageNotFoundError: + logger.debug( + "The deerflow module has no deerflow-harness distribution " + "metadata; DeerFlow instrumentation skipped. Install DeerFlow " + "2.x from the official source repository." + ) + return False + except InvalidVersion as exc: + logger.debug( + "DeerFlow has an invalid distribution version (%s); " + "instrumentation skipped.", + exc, + ) + return False + except Exception: # noqa: BLE001 + logger.debug( + "DeerFlow distribution metadata could not be read; " + "instrumentation skipped.", + exc_info=True, + ) + return False + + if not _MINIMUM_VERSION <= installed_version < _MAXIMUM_VERSION: + logger.debug( + "DeerFlow instrumentation supports deerflow-harness >=2,<3; " + "found %s. Instrumentation skipped.", + installed_version, + ) + return False + return True + + +def _instrument_dependency( + module_name: str, + class_name: str, + **kwargs: Any, +) -> BaseInstrumentor | None: + try: + module = importlib.import_module(module_name) + except ModuleNotFoundError as exc: + if exc.name == module_name or ( + exc.name is not None and module_name.startswith(f"{exc.name}.") + ): + logger.warning( + "DeerFlow instrumentation requires %s; continuing without it.", + module_name, + ) + return None + raise + + instrumentor_type = getattr(module, class_name, None) + if instrumentor_type is None: + logger.warning( + "DeerFlow instrumentation could not find %s.%s", + module_name, + class_name, + ) + return None + + instrumentor = instrumentor_type() + if instrumentor.is_instrumented_by_opentelemetry: + return None + instrumentor.instrument(**kwargs) + if instrumentor.is_instrumented_by_opentelemetry: + return instrumentor + return None + + +class DeerFlowInstrumentor(BaseInstrumentor): + """Instrument DeerFlow graphs, Gateway runs, and embedded streams.""" + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs: Any) -> None: + self._dependency_instrumentors: list[BaseInstrumentor] = [] + self._deerflow_patched = False + if not _deerflow_runtime_supported(): + return + + try: + for module_name, class_name in ( + ( + "opentelemetry.instrumentation.langchain", + "LangChainInstrumentor", + ), + ( + "opentelemetry.instrumentation.langgraph", + "LangGraphInstrumentor", + ), + ): + instrumentor = _instrument_dependency( + module_name, + class_name, + **kwargs, + ) + if instrumentor is not None: + self._dependency_instrumentors.append(instrumentor) + + from opentelemetry.util.genai.extended_handler import ( # noqa: PLC0415 + ExtendedTelemetryHandler, + ) + + handler = ExtendedTelemetryHandler( + tracer_provider=kwargs.get("tracer_provider"), + meter_provider=kwargs.get("meter_provider"), + logger_provider=kwargs.get("logger_provider"), + ) + self._deerflow_patched = instrument_deerflow(handler) + except BaseException: + uninstrument_deerflow() + owned_langchain = any( + _is_langchain_instrumentor(instrumentor) + for instrumentor in self._dependency_instrumentors + ) + for instrumentor in reversed(self._dependency_instrumentors): + instrumentor.uninstrument() + if owned_langchain: + remove_owned_langchain_alias_wrappers() + self._dependency_instrumentors = [] + raise + + def _uninstrument(self, **kwargs: Any) -> None: + del kwargs + if getattr(self, "_deerflow_patched", False): + uninstrument_deerflow() + self._deerflow_patched = False + + dependency_instrumentors = getattr( + self, "_dependency_instrumentors", [] + ) + owned_langchain = any( + _is_langchain_instrumentor(instrumentor) + for instrumentor in dependency_instrumentors + ) + for instrumentor in reversed(dependency_instrumentors): + try: + instrumentor.uninstrument() + except Exception: # noqa: BLE001 + logger.debug( + "Failed to uninstrument DeerFlow dependency %s", + instrumentor.__class__.__name__, + exc_info=True, + ) + if owned_langchain: + remove_owned_langchain_alias_wrappers() + self._dependency_instrumentors = [] diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/__init__.py new file mode 100644 index 000000000..b0a6f4284 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/__init__.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/constants.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/constants.py new file mode 100644 index 000000000..ba72cc2b8 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/constants.py @@ -0,0 +1,50 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Constants shared by DeerFlow patch helpers.""" + +AGENT_FRAMEWORK_ATTR = "_loongsuite_agent_framework" +AGENT_STEP_NODE_ATTR = "_loongsuite_agent_step_node" +DEERFLOW_AGENT_FRAMEWORK = "deerflow" +DEERFLOW_AGENT_STEP_NODE = "model" + +GEN_AI_AGENT_NAME = "gen_ai.agent.name" +GEN_AI_FRAMEWORK = "gen_ai.framework" +GEN_AI_SESSION_ID = "gen_ai.session.id" +GEN_AI_USER_ID = "gen_ai.user.id" + +DEERFLOW_ASSISTANT_ID = "deerflow.assistant.id" +DEERFLOW_RUN_ID = "deerflow.run.id" +DEERFLOW_RUN_STATUS = "deerflow.run.status" +DEERFLOW_TRACE_ID = "deerflow.trace.id" + +DEERFLOW_TRACE_METADATA_KEY = "deerflow_trace_id" + +CREATE_AGENT_ALIASES = ( + ("deerflow.agents.factory", "create_agent"), + ("deerflow.agents.lead_agent.agent", "create_agent"), + ("deerflow.client", "create_agent"), + ("deerflow.subagents.executor", "create_agent"), +) + +GATEWAY_RUN_AGENT_ALIASES = ( + ("deerflow.runtime.runs.worker", "run_agent"), + ("deerflow.runtime.runs", "run_agent"), + ("deerflow.runtime", "run_agent"), +) +GATEWAY_LOADED_RUN_AGENT_ALIASES = (("app.gateway.services", "run_agent"),) +CLIENT_STREAM = ( + "deerflow.client", + "DeerFlowClient.stream", +) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/patch.py new file mode 100644 index 000000000..cde033304 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/patch.py @@ -0,0 +1,802 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""DeerFlow graph and application-entry patches.""" + +from __future__ import annotations + +import asyncio +import importlib +import logging +import sys +import timeit +import uuid +from collections.abc import Generator, Iterator +from contextvars import Context, ContextVar, Token, copy_context +from typing import Any, Callable +from weakref import WeakKeyDictionary + +from wrapt import wrap_function_wrapper + +from opentelemetry import baggage +from opentelemetry import context as otel_context +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.util.genai.extended_handler import ( + ExtendedTelemetryHandler, +) +from opentelemetry.util.genai.extended_types import EntryInvocation +from opentelemetry.util.genai.types import Error + +from .constants import ( + AGENT_FRAMEWORK_ATTR, + AGENT_STEP_NODE_ATTR, + CLIENT_STREAM, + CREATE_AGENT_ALIASES, + DEERFLOW_AGENT_FRAMEWORK, + DEERFLOW_AGENT_STEP_NODE, + DEERFLOW_RUN_STATUS, + GATEWAY_LOADED_RUN_AGENT_ALIASES, + GATEWAY_RUN_AGENT_ALIASES, + GEN_AI_AGENT_NAME, +) +from .utils import ( + create_entry_invocation, + has_active_host_entry, + non_empty_string, + should_capture_content, + to_output_messages, + trace_id_from_sources, +) + +logger = logging.getLogger(__name__) + +_ENTRY_DEPTH: ContextVar[int] = ContextVar( + "loongsuite_deerflow_entry_depth", default=0 +) +_MISSING = object() +_LEGACY_GRAPH_ATTRS = ( + "_loongsuite_react_agent", + "_loongsuite_deepagents_agent", +) + +_patched_locations: list[tuple[str, str]] = [] +_owned_gateway_wrappers: list[Any] = [] +_original_graph_markers: WeakKeyDictionary[Any, dict[str, Any]] = ( + WeakKeyDictionary() +) +_strong_graph_markers: list[tuple[Any, dict[str, Any]]] = [] + + +class DeerFlowRunError(RuntimeError): + """A DeerFlow Gateway run ended in its error state.""" + + +class DeerFlowRunTimeout(TimeoutError): + """A DeerFlow Gateway run ended in its timeout state.""" + + +class DeerFlowRunInterrupted(InterruptedError): + """A DeerFlow Gateway run was interrupted.""" + + +def _track_graph(graph: Any, original_markers: dict[str, Any]) -> None: + try: + if graph not in _original_graph_markers: + _original_graph_markers[graph] = original_markers + return + except TypeError: + pass + + if not any(existing is graph for existing, _ in _strong_graph_markers): + _strong_graph_markers.append((graph, original_markers)) + + +def mark_deerflow_graph(graph: Any) -> Any: + """Mark a graph with DeerFlow semantics and no legacy ReAct flags.""" + marker_attrs = ( + AGENT_FRAMEWORK_ATTR, + AGENT_STEP_NODE_ATTR, + *_LEGACY_GRAPH_ATTRS, + ) + original_markers = { + name: getattr(graph, name, _MISSING) for name in marker_attrs + } + try: + setattr(graph, AGENT_FRAMEWORK_ATTR, DEERFLOW_AGENT_FRAMEWORK) + setattr(graph, AGENT_STEP_NODE_ATTR, DEERFLOW_AGENT_STEP_NODE) + for legacy_attr in _LEGACY_GRAPH_ATTRS: + if hasattr(graph, legacy_attr): + delattr(graph, legacy_attr) + except Exception: # noqa: BLE001 + logger.debug("Could not mark DeerFlow graph", exc_info=True) + return graph + _track_graph(graph, original_markers) + return graph + + +def _create_agent_alias_wrapper( + wrapped: Callable[..., Any], + _instance: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any], +) -> Any: + return mark_deerflow_graph(wrapped(*args, **kwargs)) + + +def _entry_is_active() -> bool: + return _ENTRY_DEPTH.get() > 0 or has_active_host_entry() + + +def _call_arg( + args: tuple[Any, ...], + kwargs: dict[str, Any], + position: int, + name: str, + default: Any = None, +) -> Any: + if name in kwargs: + return kwargs[name] + if len(args) > position: + return args[position] + return default + + +def _effective_user_id(fallback: Any = None) -> str | None: + if fallback is not None: + return non_empty_string(fallback) + try: + from deerflow.runtime.user_context import ( # noqa: PLC0415 + get_effective_user_id, + ) + + return non_empty_string(get_effective_user_id()) + except Exception: # noqa: BLE001 + return None + + +def _client_trace_id(instance: Any) -> str | None: + """Resolve or pre-generate current DeerFlow embedded correlation id.""" + trace_id = trace_id_from_sources() + if trace_id: + return trace_id + try: + from deerflow.config.app_config import ( # noqa: PLC0415 + is_trace_correlation_enabled, + ) + from deerflow.trace_context import generate_trace_id # noqa: PLC0415 + + if is_trace_correlation_enabled( + getattr(instance, "_app_config", None) + ): + return generate_trace_id() + except Exception: # noqa: BLE001 + logger.debug( + "Failed to resolve DeerFlow trace correlation id", + exc_info=True, + ) + return None + + +def _bind_deerflow_trace_id(trace_id: str | None) -> Any: + if not trace_id: + return None + try: + from deerflow.trace_context import ( # noqa: PLC0415 + set_current_trace_id, + ) + + return set_current_trace_id(trace_id) + except Exception: # noqa: BLE001 + logger.debug( + "Failed to bind DeerFlow trace correlation id", + exc_info=True, + ) + return None + + +def _reset_deerflow_trace_id(token: Any) -> None: + if token is None: + return + try: + from deerflow.trace_context import ( # noqa: PLC0415 + reset_current_trace_id, + ) + + reset_current_trace_id(token) + except Exception: # noqa: BLE001 + logger.debug("Failed to reset DeerFlow trace context", exc_info=True) + + +def _gateway_user_id(config: dict[str, Any]) -> str | None: + """Resolve the Gateway user through DeerFlow's runtime contract.""" + runtime_context = config.get("context") + runtime_context = ( + runtime_context if isinstance(runtime_context, dict) else {} + ) + try: + from deerflow.runtime.user_context import ( # noqa: PLC0415 + resolve_runtime_user_id, + ) + + runtime = type("_Runtime", (), {"context": runtime_context})() + return non_empty_string(resolve_runtime_user_id(runtime)) + except Exception: # noqa: BLE001 + return _effective_user_id() + + +def _gateway_invocation( + record: Any, + graph_input: Any, + config: Any, +) -> EntryInvocation: + config = config if isinstance(config, dict) else {} + metadata = config.get("metadata") + metadata = metadata if isinstance(metadata, dict) else {} + record_metadata = getattr(record, "metadata", None) + record_metadata = ( + record_metadata if isinstance(record_metadata, dict) else {} + ) + context = config.get("context") + context = context if isinstance(context, dict) else {} + configurable = config.get("configurable") + configurable = configurable if isinstance(configurable, dict) else {} + run_name = config.get("run_name") or metadata.get("run_name") + assistant_id = getattr(record, "assistant_id", None) + agent_name = ( + context.get("agent_name") + or configurable.get("agent_name") + or metadata.get("agent_name") + or run_name + or "lead-agent" + ) + + return create_entry_invocation( + thread_id=getattr(record, "thread_id", None), + user_id=_gateway_user_id(config), + agent_name=agent_name, + assistant_id=assistant_id, + run_id=getattr(record, "run_id", None), + deerflow_trace_id=trace_id_from_sources( + metadata, + record_metadata, + ), + input_value=graph_input, + ) + + +def _error_from_exception(exc: BaseException) -> Error: + return Error(message=str(exc), type=type(exc)) + + +def _run_status_value(record: Any) -> str: + status = getattr(record, "status", None) + value = getattr(status, "value", status) + return non_empty_string(value) or "unknown" + + +def _run_status_error(record: Any, status: str) -> BaseException: + message = non_empty_string(getattr(record, "error", None)) + if status == "timeout": + return DeerFlowRunTimeout(message or "DeerFlow run timed out") + if status == "interrupted": + return DeerFlowRunInterrupted( + message or "DeerFlow run was interrupted" + ) + return DeerFlowRunError( + message or f"DeerFlow run ended with status {status}" + ) + + +def _safe_stop_entry( + handler: ExtendedTelemetryHandler, + invocation: EntryInvocation, +) -> None: + try: + handler.stop_entry(invocation) + except Exception: # noqa: BLE001 + logger.debug("Failed to stop DeerFlow ENTRY", exc_info=True) + + +def _safe_fail_entry( + handler: ExtendedTelemetryHandler, + invocation: EntryInvocation, + exc: BaseException, +) -> None: + try: + handler.fail_entry(invocation, _error_from_exception(exc)) + except Exception: # noqa: BLE001 + logger.debug("Failed to fail DeerFlow ENTRY", exc_info=True) + + +def _start_entry_with_agent_baggage( + handler: ExtendedTelemetryHandler, + invocation: EntryInvocation, +) -> None: + """Start ENTRY while exposing its resolved agent name to child spans.""" + parent_context = otel_context.get_current() + agent_name = invocation.attributes.get(GEN_AI_AGENT_NAME) + if agent_name: + parent_context = baggage.set_baggage( + GEN_AI_AGENT_NAME, + agent_name, + parent_context, + ) + handler.start_entry(invocation, context=parent_context) + + +class _GatewayRunAgentWrapper: + """Create one ENTRY around the asynchronous Gateway worker run.""" + + def __init__(self, handler: ExtendedTelemetryHandler): + self._handler = handler + + async def __call__( + self, + wrapped: Callable[..., Any], + _instance: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> Any: + if _entry_is_active(): + return await wrapped(*args, **kwargs) + + record = _call_arg(args, kwargs, 2, "record") + # DeerFlow 2.x declares these as keyword-only arguments. Reading them + # by name keeps this wrapper aligned with the supported API instead of + # relying on impossible positional offsets. + graph_input = kwargs.get("graph_input") + config = kwargs.get("config", {}) + if record is None: + return await wrapped(*args, **kwargs) + + invocation = _gateway_invocation(record, graph_input, config) + depth_token = _ENTRY_DEPTH.set(_ENTRY_DEPTH.get() + 1) + try: + _start_entry_with_agent_baggage(self._handler, invocation) + except Exception: # noqa: BLE001 + _ENTRY_DEPTH.reset(depth_token) + logger.debug( + "Failed to start DeerFlow Gateway ENTRY", exc_info=True + ) + return await wrapped(*args, **kwargs) + + try: + result = await wrapped(*args, **kwargs) + except BaseException as exc: + status = _run_status_value(record) + if status in {"unknown", "pending", "running"}: + status = ( + "interrupted" + if isinstance(exc, asyncio.CancelledError) + else "error" + ) + invocation.attributes[DEERFLOW_RUN_STATUS] = status + if status == "interrupted": + _safe_stop_entry(self._handler, invocation) + else: + _safe_fail_entry(self._handler, invocation, exc) + raise + else: + status = _run_status_value(record) + invocation.attributes[DEERFLOW_RUN_STATUS] = status + if should_capture_content(): + invocation.output_messages = to_output_messages( + getattr(record, "last_ai_message", None) + ) + if status in {"success", "interrupted"}: + _safe_stop_entry(self._handler, invocation) + else: + _safe_fail_entry( + self._handler, + invocation, + _run_status_error(record, status), + ) + return result + finally: + _ENTRY_DEPTH.reset(depth_token) + + +class _StreamOutput: + """Accumulate the final embedded assistant message when capture is on.""" + + def __init__(self) -> None: + self._chunks: dict[str, list[str]] = {} + self._last_id: str | None = None + + def observe( + self, + event: Any, + invocation: EntryInvocation, + ) -> None: + if getattr(event, "type", None) != "messages-tuple": + return + data = getattr(event, "data", None) + if not isinstance(data, dict) or data.get("type") != "ai": + return + if invocation.response_time_to_first_token is None: + invocation.response_time_to_first_token = int( + (timeit.default_timer() - invocation.monotonic_start_s) + * 1_000_000_000 + ) + raw_content = data.get("content") + if not isinstance(raw_content, str) or not raw_content: + return + if not should_capture_content(): + return + content = raw_content + message_id = non_empty_string(data.get("id")) or "" + self._chunks.setdefault(message_id, []).append(content) + self._last_id = message_id + + def finish(self, invocation: EntryInvocation) -> None: + if not should_capture_content() or self._last_id is None: + return + output = "".join(self._chunks.get(self._last_id, ())) + invocation.output_messages = to_output_messages(output) + + +def _start_isolated_entry( + handler: ExtendedTelemetryHandler, + invocation: EntryInvocation, +) -> Token[int]: + depth_token = _ENTRY_DEPTH.set(_ENTRY_DEPTH.get() + 1) + try: + _start_entry_with_agent_baggage(handler, invocation) + except BaseException: + _ENTRY_DEPTH.reset(depth_token) + raise + return depth_token + + +def _finish_isolated_entry( + handler: ExtendedTelemetryHandler, + invocation: EntryInvocation, + depth_token: Token[int], + deerflow_trace_token: Any, + error: BaseException | None, +) -> None: + try: + if error is None: + _safe_stop_entry(handler, invocation) + else: + if isinstance(error, (GeneratorExit, asyncio.CancelledError)): + status = "interrupted" + elif isinstance(error, TimeoutError): + status = "timeout" + else: + status = "error" + invocation.attributes[DEERFLOW_RUN_STATUS] = status + if status == "interrupted": + _safe_stop_entry(handler, invocation) + else: + _safe_fail_entry(handler, invocation, error) + finally: + _ENTRY_DEPTH.reset(depth_token) + _reset_deerflow_trace_id(deerflow_trace_token) + + +def _close_iterator(inner: Iterator[Any]) -> None: + close = getattr(inner, "close", None) + if callable(close): + close() + + +def _iterate_in_context( + inner: Iterator[Any], + stream_context: Context, +) -> Generator[Any, None, None]: + """Advance and close an iterator inside its call-time context.""" + try: + while True: + try: + event = stream_context.run(next, inner) + except StopIteration: + break + yield event + finally: + try: + stream_context.run(_close_iterator, inner) + except Exception: # noqa: BLE001 + logger.debug("Failed to close DeerFlow stream", exc_info=True) + + +def _entry_stream( + inner: Iterator[Any], + handler: ExtendedTelemetryHandler, + invocation: EntryInvocation, + entry_context: Context, + deerflow_trace_id: str | None, +) -> Generator[Any, None, None]: + """Advance a DeerFlow iterator only inside an isolated OTel context.""" + output = _StreamOutput() + deerflow_trace_token = entry_context.run( + _bind_deerflow_trace_id, + deerflow_trace_id, + ) + try: + depth_token = entry_context.run( + _start_isolated_entry, + handler, + invocation, + ) + except Exception: # noqa: BLE001 + entry_context.run( + _reset_deerflow_trace_id, + deerflow_trace_token, + ) + logger.debug("Failed to start DeerFlow Client ENTRY", exc_info=True) + yield from _iterate_in_context(inner, entry_context) + return + + try: + while True: + try: + event = entry_context.run(next, inner) + except StopIteration: + break + output.observe(event, invocation) + yield event + except GeneratorExit as exc: + try: + entry_context.run(_close_iterator, inner) + except Exception: # noqa: BLE001 + logger.debug("Failed to close DeerFlow stream", exc_info=True) + entry_context.run( + _finish_isolated_entry, + handler, + invocation, + depth_token, + deerflow_trace_token, + exc, + ) + raise + except BaseException as exc: + entry_context.run( + _finish_isolated_entry, + handler, + invocation, + depth_token, + deerflow_trace_token, + exc, + ) + raise + else: + output.finish(invocation) + invocation.attributes[DEERFLOW_RUN_STATUS] = "success" + entry_context.run( + _finish_isolated_entry, + handler, + invocation, + depth_token, + deerflow_trace_token, + None, + ) + + +class _ClientStreamWrapper: + """Create an isolated, iterator-lifecycle ENTRY for embedded streams.""" + + def __init__(self, handler: ExtendedTelemetryHandler): + self._handler = handler + + def __call__( + self, + wrapped: Callable[..., Any], + instance: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> Any: + entry_context = copy_context() + if _entry_is_active(): + inner = iter(wrapped(*args, **kwargs)) + return _iterate_in_context(inner, entry_context) + + call_kwargs = dict(kwargs) + thread_id = _call_arg(args, call_kwargs, 1, "thread_id") + if thread_id is None: + thread_id = str(uuid.uuid4()) + call_kwargs["thread_id"] = thread_id + + message = _call_arg(args, call_kwargs, 0, "message") + assistant_id = getattr(instance, "_agent_name", None) + agent_name = ( + assistant_id or call_kwargs.get("run_name") or "lead-agent" + ) + deerflow_trace_id = _client_trace_id(instance) + invocation = create_entry_invocation( + thread_id=thread_id, + user_id=_effective_user_id(), + agent_name=agent_name, + assistant_id=assistant_id, + deerflow_trace_id=deerflow_trace_id, + input_value=message, + ) + inner = iter(wrapped(*args, **call_kwargs)) + return _entry_stream( + inner, + self._handler, + invocation, + entry_context, + deerflow_trace_id, + ) + + +def _patch_location( + module_name: str, + target: str, + wrapper: Any, +) -> bool: + if (module_name, target) in _patched_locations: + return False + try: + importlib.import_module(module_name) + wrap_function_wrapper(module_name, target, wrapper) + except Exception: # noqa: BLE001 + logger.warning( + "Could not patch DeerFlow target %s.%s", + module_name, + target, + exc_info=True, + ) + return False + _patched_locations.append((module_name, target)) + return True + + +def instrument_deerflow(handler: ExtendedTelemetryHandler) -> bool: + """Patch installed DeerFlow 2.x creation and application entry points.""" + patched = False + for module_name, target in CREATE_AGENT_ALIASES: + patched = ( + _patch_location( + module_name, + target, + _create_agent_alias_wrapper, + ) + or patched + ) + + gateway_wrapper = _GatewayRunAgentWrapper(handler) + for module_name, target in GATEWAY_RUN_AGENT_ALIASES: + location_patched = _patch_location( + module_name, + target, + gateway_wrapper, + ) + if location_patched: + owner, attribute = _resolve_patch_owner(module_name, target) + _owned_gateway_wrappers.append(getattr(owner, attribute)) + patched = location_patched or patched + for module_name, target in GATEWAY_LOADED_RUN_AGENT_ALIASES: + if module_name in sys.modules: + location_patched = _patch_location( + module_name, + target, + gateway_wrapper, + ) + if location_patched: + owner, attribute = _resolve_patch_owner(module_name, target) + _owned_gateway_wrappers.append(getattr(owner, attribute)) + patched = location_patched or patched + patched = ( + _patch_location( + *CLIENT_STREAM, + _ClientStreamWrapper(handler), + ) + or patched + ) + return patched + + +def _resolve_patch_owner(module_name: str, target: str) -> tuple[Any, str]: + owner: Any = importlib.import_module(module_name) + parts = target.split(".") + for part in parts[:-1]: + owner = getattr(owner, part) + return owner, parts[-1] + + +def _restore_graphs() -> None: + tracked = [ + *_original_graph_markers.items(), + *_strong_graph_markers, + ] + for graph, original_markers in tracked: + for attr_name, original_value in original_markers.items(): + try: + if original_value is _MISSING: + if hasattr(graph, attr_name): + delattr(graph, attr_name) + else: + setattr(graph, attr_name, original_value) + except Exception: # noqa: BLE001 + logger.debug( + "Could not restore DeerFlow graph marker", + exc_info=True, + ) + _original_graph_markers.clear() + _strong_graph_markers.clear() + + +def _restore_late_gateway_aliases() -> None: + """Unwrap Gateway aliases cached after instrumentation completed.""" + patched_locations = set(_patched_locations) + for module_name, target in GATEWAY_LOADED_RUN_AGENT_ALIASES: + if ( + module_name not in sys.modules + or (module_name, target) in patched_locations + ): + continue + try: + owner, attribute = _resolve_patch_owner(module_name, target) + candidate = getattr(owner, attribute) + if any( + candidate is wrapper for wrapper in _owned_gateway_wrappers + ): + unwrap(owner, attribute) + except Exception: # noqa: BLE001 + logger.debug( + "Could not restore late DeerFlow Gateway alias %s.%s", + module_name, + target, + exc_info=True, + ) + + +def remove_owned_langchain_alias_wrappers() -> None: + """Remove alias wrappers installed by a DeerFlow-owned dependency. + + ``LangChainInstrumentor.uninstrument()`` restores its canonical module + targets, but DeerFlow modules may have cached the wrapped ``create_agent`` + callable during import. Those cached aliases are not instrumentor state; + remove only the matching LangChain wrapper from those locations. + """ + for module_name, target in CREATE_AGENT_ALIASES: + try: + owner, attribute = _resolve_patch_owner(module_name, target) + candidate = getattr(owner, attribute) + wrapper = getattr(candidate, "_self_wrapper", None) + if ( + getattr(wrapper, "__module__", None) + == "opentelemetry.instrumentation.langchain" + and getattr(wrapper, "__name__", None) + == "_create_agent_wrapper" + ): + unwrap(owner, attribute) + except Exception: # noqa: BLE001 + logger.debug( + "Could not restore DeerFlow dependency alias %s.%s", + module_name, + target, + exc_info=True, + ) + + +def uninstrument_deerflow() -> None: + """Restore all DeerFlow module targets and existing graph markers.""" + _restore_late_gateway_aliases() + for module_name, target in reversed(_patched_locations): + try: + owner, attribute = _resolve_patch_owner(module_name, target) + unwrap(owner, attribute) + except Exception: # noqa: BLE001 + logger.debug( + "Could not restore DeerFlow target %s.%s", + module_name, + target, + exc_info=True, + ) + _patched_locations.clear() + _owned_gateway_wrappers.clear() + _restore_graphs() diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/utils.py new file mode 100644 index 000000000..340071add --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/utils.py @@ -0,0 +1,258 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Identity and content helpers for DeerFlow ENTRY spans.""" + +from __future__ import annotations + +import logging +from typing import Any, Iterable + +from opentelemetry import baggage, trace +from opentelemetry.util.genai.extended_handler import is_entry_context_active +from opentelemetry.util.genai.extended_types import EntryInvocation +from opentelemetry.util.genai.types import ( + ContentCapturingMode, + InputMessage, + OutputMessage, + Text, +) +from opentelemetry.util.genai.utils import ( + get_content_capturing_mode, + is_experimental_mode, +) + +from .constants import ( + DEERFLOW_ASSISTANT_ID, + DEERFLOW_RUN_ID, + DEERFLOW_TRACE_ID, + DEERFLOW_TRACE_METADATA_KEY, + GEN_AI_AGENT_NAME, + GEN_AI_FRAMEWORK, + GEN_AI_SESSION_ID, + GEN_AI_USER_ID, +) + +logger = logging.getLogger(__name__) + + +def non_empty_string(value: Any) -> str | None: + """Return a stripped string value, or ``None`` for empty inputs.""" + if value is None: + return None + try: + text = str(value).strip() + except Exception: # noqa: BLE001 + return None + return text or None + + +def should_capture_content() -> bool: + """Whether sensitive message content may be written to spans.""" + if not is_experimental_mode(): + return False + try: + return get_content_capturing_mode() in ( + ContentCapturingMode.SPAN_ONLY, + ContentCapturingMode.SPAN_AND_EVENT, + ) + except ValueError: + return False + + +def baggage_identity(key: str) -> str | None: + """Read a non-empty GenAI identity value from current baggage.""" + return non_empty_string(baggage.get_baggage(key)) + + +def resolve_session_id(thread_id: Any) -> str | None: + """Apply the ENTRY session precedence contract.""" + return baggage_identity(GEN_AI_SESSION_ID) or non_empty_string(thread_id) + + +def resolve_user_id(deerflow_user_id: Any) -> str | None: + """Apply the ENTRY user precedence contract.""" + return baggage_identity(GEN_AI_USER_ID) or non_empty_string( + deerflow_user_id + ) + + +def _message_role(message: Any) -> str: + if isinstance(message, dict): + role = message.get("role") or message.get("type") + else: + role = getattr(message, "role", None) or getattr(message, "type", None) + role_text = non_empty_string(role) or "user" + return { + "ai": "assistant", + "human": "user", + }.get(role_text, role_text) + + +def _message_content(message: Any) -> str | None: + if isinstance(message, str): + return non_empty_string(message) + if isinstance(message, dict): + content = message.get("content") + else: + content = getattr(message, "content", None) + + if isinstance(content, str): + return non_empty_string(content) + if isinstance(content, list): + parts: list[str] = [] + for part in content: + if isinstance(part, str): + text = non_empty_string(part) + elif isinstance(part, dict): + text = non_empty_string(part.get("text")) + else: + text = non_empty_string(getattr(part, "text", None)) + if text: + parts.append(text) + return "".join(parts) or None + return non_empty_string(content) + + +def to_input_messages(value: Any) -> list[InputMessage]: + """Convert common DeerFlow/LangChain input shapes to GenAI messages.""" + if not should_capture_content(): + return [] + + messages: Iterable[Any] + if isinstance(value, dict): + raw_messages = value.get("messages") + if isinstance(raw_messages, (list, tuple)): + messages = raw_messages + else: + direct = value.get("input") or value.get("query") + messages = [direct] if direct is not None else [] + elif isinstance(value, (list, tuple)): + messages = value + else: + messages = [value] + + converted: list[InputMessage] = [] + for message in messages: + content = _message_content(message) + if content: + converted.append( + InputMessage( + role=_message_role(message), + parts=[Text(content=content)], + ) + ) + return converted + + +def to_output_messages(value: Any) -> list[OutputMessage]: + """Convert a final DeerFlow response to a GenAI output message.""" + if not should_capture_content(): + return [] + content = _message_content(value) + if not content: + return [] + return [ + OutputMessage( + role="assistant", + parts=[Text(content=content)], + finish_reason="stop", + ) + ] + + +def trace_id_from_sources(*sources: Any) -> str | None: + """Resolve DeerFlow's correlation id without treating it as an OTel id.""" + + def normalize(value: Any) -> str | None: + try: + from deerflow.trace_context import ( # noqa: PLC0415 + normalize_trace_id, + ) + + return normalize_trace_id(value) + except (ImportError, ModuleNotFoundError): + return non_empty_string(value) + + for source in sources: + if isinstance(source, dict): + value = source.get(DEERFLOW_TRACE_METADATA_KEY) or source.get( + DEERFLOW_TRACE_ID + ) + text = normalize(value) + if text: + return text + try: + from deerflow.trace_context import ( # noqa: PLC0415 + get_current_trace_id, + ) + + return non_empty_string(get_current_trace_id()) + except Exception: # noqa: BLE001 + return None + + +def create_entry_invocation( + *, + thread_id: Any, + user_id: Any, + agent_name: Any, + assistant_id: Any = None, + run_id: Any = None, + deerflow_trace_id: Any = None, + input_value: Any = None, +) -> EntryInvocation: + """Build a DeerFlow ENTRY invocation with stable identity attributes.""" + resolved_agent_name = non_empty_string(agent_name) or "lead-agent" + resolved_assistant_id = non_empty_string(assistant_id) + resolved_run_id = non_empty_string(run_id) + resolved_trace_id = non_empty_string(deerflow_trace_id) + + attributes: dict[str, Any] = { + GEN_AI_FRAMEWORK: "deerflow", + GEN_AI_AGENT_NAME: resolved_agent_name, + } + if resolved_assistant_id: + attributes[DEERFLOW_ASSISTANT_ID] = resolved_assistant_id + if resolved_run_id: + attributes[DEERFLOW_RUN_ID] = resolved_run_id + if resolved_trace_id: + attributes[DEERFLOW_TRACE_ID] = resolved_trace_id + + return EntryInvocation( + session_id=resolve_session_id(thread_id), + user_id=resolve_user_id(user_id), + input_messages=to_input_messages(input_value), + attributes=attributes, + ) + + +def has_active_host_entry() -> bool: + """Best-effort detection of an ENTRY created by another instrumentor.""" + if is_entry_context_active(): + return True + + span = trace.get_current_span() + if getattr(span, "name", None) == "enter_ai_application_system": + return True + + attributes = getattr(span, "attributes", None) + if attributes is None: + attributes = getattr(span, "_attributes", None) + if isinstance(attributes, dict): + return attributes.get("gen_ai.span.kind") == "ENTRY" + try: + return attributes.get("gen_ai.span.kind") == "ENTRY" + except (AttributeError, TypeError): + return False diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/package.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/package.py new file mode 100644 index 000000000..807e494d1 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/package.py @@ -0,0 +1,20 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DeerFlow 2.x is installed from the official Git repository. The package +# named deerflow-harness on PyPI is an unrelated placeholder, so dependency +# validation must happen at runtime instead of through BaseInstrumentor. +_instruments: tuple[str, ...] = () + +_supports_metrics = False diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/version.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/version.py new file mode 100644 index 000000000..5b84a9217 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.8.0.dev" diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/conftest.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/conftest.py new file mode 100644 index 000000000..80e43db2c --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/conftest.py @@ -0,0 +1,69 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test configuration for DeerFlow instrumentation.""" + +from __future__ import annotations + +import os + +import pytest + +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.util.genai.environment_variables import ( + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, +) +from opentelemetry.util.genai.extended_handler import ( + ExtendedTelemetryHandler, +) + + +def pytest_configure(config: pytest.Config) -> None: + del config + os.environ["OTEL_SEMCONV_STABILITY_OPT_IN"] = "gen_ai_latest_experimental" + + +@pytest.fixture +def span_exporter() -> InMemorySpanExporter: + return InMemorySpanExporter() + + +@pytest.fixture +def tracer_provider(span_exporter: InMemorySpanExporter) -> TracerProvider: + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(span_exporter)) + return provider + + +@pytest.fixture +def handler(tracer_provider: TracerProvider) -> ExtendedTelemetryHandler: + return ExtendedTelemetryHandler(tracer_provider=tracer_provider) + + +@pytest.fixture +def capture_content() -> None: + os.environ[OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT] = ( + "SPAN_ONLY" + ) + try: + yield + finally: + os.environ.pop( + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, + None, + ) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.latest.txt b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.latest.txt new file mode 100644 index 000000000..3a16c8c60 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.latest.txt @@ -0,0 +1,10 @@ +deerflow-harness @ git+https://github.com/bytedance/deer-flow.git@24648194aeb1488c1351b38ca65b81c08a650050#subdirectory=backend/packages/harness +pytest +pytest-asyncio +wrapt<2.0.0 + +-e opentelemetry-instrumentation +-e util/opentelemetry-util-genai +-e instrumentation-loongsuite/loongsuite-instrumentation-langchain +-e instrumentation-loongsuite/loongsuite-instrumentation-langgraph +-e instrumentation-loongsuite/loongsuite-instrumentation-deerflow diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.oldest.txt b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.oldest.txt new file mode 100644 index 000000000..08cc3266e --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.oldest.txt @@ -0,0 +1,10 @@ +deerflow-harness @ git+https://github.com/bytedance/deer-flow.git@7e7f0410797693cf882594555ba414e0361d4c6f#subdirectory=backend/packages/harness +pytest +pytest-asyncio +wrapt<2.0.0 + +-e opentelemetry-instrumentation +-e util/opentelemetry-util-genai +-e instrumentation-loongsuite/loongsuite-instrumentation-langchain +-e instrumentation-loongsuite/loongsuite-instrumentation-langgraph +-e instrumentation-loongsuite/loongsuite-instrumentation-deerflow diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py new file mode 100644 index 000000000..ad6254e3d --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py @@ -0,0 +1,874 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Focused lifecycle tests for DeerFlow application ENTRY wrappers.""" + +from __future__ import annotations + +import asyncio +import json +import logging +import sys +import uuid +from concurrent.futures import ThreadPoolExecutor +from contextvars import Context as ContextVarsContext +from contextvars import ContextVar +from threading import Barrier +from types import ModuleType, SimpleNamespace +from typing import Any + +import pytest + +from opentelemetry import baggage, context, trace +from opentelemetry.instrumentation.deerflow.internal.constants import ( + DEERFLOW_RUN_STATUS, + GEN_AI_SESSION_ID, + GEN_AI_USER_ID, +) +from opentelemetry.instrumentation.deerflow.internal.patch import ( + _ClientStreamWrapper, + _GatewayRunAgentWrapper, +) +from opentelemetry.trace import StatusCode +from opentelemetry.util.genai.extended_types import EntryInvocation + + +class _Event: + def __init__(self, event_type: str, data: dict[str, Any]): + self.type = event_type + self.data = data + + +class _Client: + _agent_name = "research-agent" + + +class _DefaultClient: + _agent_name = None + + +def _entry_spans(span_exporter): + return [ + span + for span in span_exporter.get_finished_spans() + if span.attributes.get("gen_ai.span.kind") == "ENTRY" + ] + + +def test_client_stream_uses_isolated_context_and_generated_thread_id( + handler, + tracer_provider, + span_exporter, + capture_content, + monkeypatch, +): + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.internal.patch._effective_user_id", + lambda fallback=None: fallback or "deerflow-user", + ) + observed: dict[str, Any] = {} + + def stream(message, *, thread_id=None, **_kwargs): + observed["message"] = message + observed["thread_id"] = thread_id + observed["span_during_next"] = ( + trace.get_current_span().get_span_context().is_valid + ) + yield _Event( + "messages-tuple", + {"type": "ai", "id": "answer", "content": "hello "}, + ) + observed["span_during_resume"] = ( + trace.get_current_span().get_span_context().is_valid + ) + yield _Event( + "messages-tuple", + {"type": "ai", "id": "answer", "content": "world"}, + ) + + wrapper = _ClientStreamWrapper(handler) + result = wrapper(stream, _Client(), ("question",), {}) + + assert not trace.get_current_span().get_span_context().is_valid + first = next(result) + assert first.data["content"] == "hello " + assert not trace.get_current_span().get_span_context().is_valid + with tracer_provider.get_tracer(__name__).start_as_current_span( + "consumer-between-yields" + ) as consumer_span: + consumer_context = consumer_span.get_span_context() + second = next(result) + assert second.data["content"] == "world" + assert list(result) == [] + assert not trace.get_current_span().get_span_context().is_valid + + assert observed["message"] == "question" + uuid.UUID(observed["thread_id"]) + assert observed["span_during_next"] is True + assert observed["span_during_resume"] is True + + entry = _entry_spans(span_exporter)[0] + assert consumer_context.trace_id != entry.context.trace_id + assert consumer_context.span_id != entry.context.span_id + assert entry.attributes[GEN_AI_SESSION_ID] == observed["thread_id"] + assert entry.attributes[GEN_AI_USER_ID] == "deerflow-user" + assert entry.attributes["gen_ai.framework"] == "deerflow" + assert entry.attributes["gen_ai.agent.name"] == "research-agent" + assert entry.attributes["deerflow.assistant.id"] == "research-agent" + assert entry.attributes[DEERFLOW_RUN_STATUS] == "success" + assert entry.attributes["gen_ai.response.time_to_first_token"] >= 0 + assert ( + json.loads(entry.attributes["gen_ai.input.messages"])[0]["parts"][0][ + "content" + ] + == "question" + ) + assert ( + json.loads(entry.attributes["gen_ai.output.messages"])[0]["parts"][0][ + "content" + ] + == "hello world" + ) + + +def test_client_stream_never_consumed_does_not_create_entry( + handler, + span_exporter, +): + observed = {"started": False} + + def stream(_message, *, thread_id=None): + del thread_id + observed["started"] = True + yield _Event("end", {}) + + result = _ClientStreamWrapper(handler)(stream, _Client(), ("q",), {}) + + assert observed["started"] is False + assert _entry_spans(span_exporter) == [] + result.close() + assert observed["started"] is False + assert _entry_spans(span_exporter) == [] + + +def test_client_stream_keeps_caller_parent_when_consumed_in_other_thread( + handler, + tracer_provider, + span_exporter, +): + tracer = tracer_provider.get_tracer(__name__) + wrapper = _ClientStreamWrapper(handler) + + def stream(_message, *, thread_id=None): + assert thread_id == "cross-thread" + yield _Event("end", {}) + + with tracer.start_as_current_span("producer-caller") as caller: + caller_context = caller.get_span_context() + result = wrapper( + stream, + _Client(), + ("question",), + {"thread_id": "cross-thread"}, + ) + + def consume() -> tuple[int, int]: + with tracer.start_as_current_span("consumer") as consumer: + consumer_context = consumer.get_span_context() + list(result) + return consumer_context.trace_id, consumer_context.span_id + + with ThreadPoolExecutor(max_workers=1) as executor: + consumer_trace_id, consumer_span_id = executor.submit(consume).result( + timeout=10 + ) + + entry = _entry_spans(span_exporter)[0] + assert entry.context.trace_id == caller_context.trace_id + assert entry.parent.span_id == caller_context.span_id + assert entry.context.trace_id != consumer_trace_id + assert entry.parent.span_id != consumer_span_id + + +def test_client_stream_baggage_identity_has_precedence( + handler, + span_exporter, + monkeypatch, +): + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.internal.patch._effective_user_id", + lambda fallback=None: fallback or "deerflow-user", + ) + + def stream(_message, *, thread_id=None): + assert thread_id == "deerflow-thread" + yield _Event("end", {}) + + ctx = baggage.set_baggage(GEN_AI_SESSION_ID, "host-session") + ctx = baggage.set_baggage(GEN_AI_USER_ID, "host-user", ctx) + token = context.attach(ctx) + try: + result = _ClientStreamWrapper(handler)( + stream, + _Client(), + ("question",), + {"thread_id": "deerflow-thread"}, + ) + list(result) + finally: + context.detach(token) + + entry = _entry_spans(span_exporter)[0] + assert entry.attributes[GEN_AI_SESSION_ID] == "host-session" + assert entry.attributes[GEN_AI_USER_ID] == "host-user" + + +def test_default_client_does_not_invent_assistant_id(handler, span_exporter): + result = _ClientStreamWrapper(handler)( + lambda *_args, **_kwargs: iter([_Event("end", {})]), + _DefaultClient(), + ("q",), + {}, + ) + list(result) + + entry = _entry_spans(span_exporter)[0] + assert entry.attributes["gen_ai.agent.name"] == "lead-agent" + assert "deerflow.assistant.id" not in entry.attributes + + +def test_client_stream_close_interrupts_entry_without_error( + handler, + span_exporter, +): + observed = {"closed": False} + + def stream(_message, *, thread_id=None): + del thread_id + try: + yield _Event("messages-tuple", {"type": "ai", "content": "x"}) + yield _Event("end", {}) + finally: + observed["closed"] = True + + result = _ClientStreamWrapper(handler)(stream, _Client(), ("q",), {}) + next(result) + result.close() + + assert observed["closed"] is True + assert not trace.get_current_span().get_span_context().is_valid + entry = _entry_spans(span_exporter)[0] + assert entry.status.status_code == StatusCode.UNSET + assert "error.type" not in entry.attributes + assert entry.attributes[DEERFLOW_RUN_STATUS] == "interrupted" + + +@pytest.mark.parametrize( + ("exception_type", "expected_status"), + [ + (ValueError, "error"), + (TimeoutError, "timeout"), + ], +) +def test_client_stream_exception_fails_entry_with_mapped_status( + exception_type, + expected_status, + handler, + span_exporter, +): + def stream(_message, *, thread_id=None): + del thread_id + yield _Event("messages-tuple", {"type": "ai", "content": "x"}) + raise exception_type("stream failed") + + result = _ClientStreamWrapper(handler)(stream, _Client(), ("q",), {}) + next(result) + with pytest.raises(exception_type, match="stream failed"): + next(result) + + entry = _entry_spans(span_exporter)[0] + assert entry.status.status_code == StatusCode.ERROR + assert entry.attributes["error.type"] == exception_type.__name__ + assert entry.attributes[DEERFLOW_RUN_STATUS] == expected_status + + +def test_client_stream_reuses_active_host_entry(handler, span_exporter): + host = EntryInvocation(session_id="host") + handler.start_entry(host) + try: + result = _ClientStreamWrapper(handler)( + lambda *_args, **_kwargs: iter([_Event("end", {})]), + _Client(), + ("q",), + {}, + ) + list(result) + finally: + handler.stop_entry(host) + + assert len(_entry_spans(span_exporter)) == 1 + + +def test_client_stream_reuses_host_entry_under_current_child_span( + handler, + tracer_provider, + span_exporter, +): + tracer = tracer_provider.get_tracer(__name__) + observed = {} + + def stream(_message, *, thread_id=None): + del thread_id + observed["span_id"] = ( + trace.get_current_span().get_span_context().span_id + ) + yield _Event("end", {}) + + host = EntryInvocation(session_id="host") + handler.start_entry(host) + try: + with tracer.start_as_current_span("host-child") as child: + child_span_id = child.get_span_context().span_id + result = _ClientStreamWrapper(handler)( + stream, + _Client(), + ("q",), + {}, + ) + list(result) + finally: + handler.stop_entry(host) + + assert observed["span_id"] == child_span_id + assert len(_entry_spans(span_exporter)) == 1 + + +def test_client_stream_reuses_host_entry_across_consumer_context( + handler, + tracer_provider, + span_exporter, +): + tracer = tracer_provider.get_tracer(__name__) + observed = {} + + def stream(_message, *, thread_id=None): + del thread_id + observed["session_id"] = baggage.get_baggage(GEN_AI_SESSION_ID) + observed["user_id"] = baggage.get_baggage(GEN_AI_USER_ID) + with tracer.start_as_current_span("hosted-stream-work"): + yield _Event("end", {}) + + host = EntryInvocation(session_id="host-session", user_id="host-user") + handler.start_entry(host) + try: + result = _ClientStreamWrapper(handler)( + stream, + _Client(), + ("q",), + {}, + ) + with ThreadPoolExecutor(max_workers=1) as executor: + executor.submit( + ContextVarsContext().run, + lambda: list(result), + ).result(timeout=10) + finally: + handler.stop_entry(host) + + entry = _entry_spans(span_exporter)[0] + work = next( + span + for span in span_exporter.get_finished_spans() + if span.name == "hosted-stream-work" + ) + assert len(_entry_spans(span_exporter)) == 1 + assert work.context.trace_id == entry.context.trace_id + assert work.parent.span_id == entry.context.span_id + assert observed == { + "session_id": "host-session", + "user_id": "host-user", + } + + +def test_client_chat_reuses_stream_and_creates_one_entry( + handler, + span_exporter, +): + class ChatClient: + _agent_name = "chat-agent" + + def __init__(self): + self.stream_calls = 0 + self._stream_wrapper = _ClientStreamWrapper(handler) + + def _raw_stream(self, message, *, thread_id=None): + self.stream_calls += 1 + assert message == "question" + assert thread_id == "chat-thread" + yield _Event( + "messages-tuple", + {"type": "ai", "id": "answer", "content": "answer"}, + ) + + def stream(self, message, *, thread_id=None): + return self._stream_wrapper( + self._raw_stream, + self, + (message,), + {"thread_id": thread_id}, + ) + + def chat(self, message, *, thread_id=None): + return "".join( + event.data.get("content", "") + for event in self.stream(message, thread_id=thread_id) + ) + + client = ChatClient() + + assert client.chat("question", thread_id="chat-thread") == "answer" + assert client.stream_calls == 1 + entries = _entry_spans(span_exporter) + assert len(entries) == 1 + assert entries[0].attributes[GEN_AI_SESSION_ID] == "chat-thread" + assert entries[0].attributes[DEERFLOW_RUN_STATUS] == "success" + + +def test_client_stream_concurrent_threads_isolate_identity_and_trace( + handler, + tracer_provider, + span_exporter, +): + barrier = Barrier(2) + wrapper = _ClientStreamWrapper(handler) + tracer = tracer_provider.get_tracer(__name__) + + def consume(suffix: str) -> None: + ctx = baggage.set_baggage( + GEN_AI_SESSION_ID, + f"baggage-session-{suffix}", + ) + ctx = baggage.set_baggage( + GEN_AI_USER_ID, + f"baggage-user-{suffix}", + ctx, + ) + token = context.attach(ctx) + try: + + def stream(_message, *, thread_id=None): + assert thread_id == f"deerflow-thread-{suffix}" + barrier.wait(timeout=5) + with tracer.start_as_current_span(f"embedded-child-{suffix}"): + yield _Event("end", {}) + + result = wrapper( + stream, + _Client(), + (f"question-{suffix}",), + {"thread_id": f"deerflow-thread-{suffix}"}, + ) + list(result) + assert not trace.get_current_span().get_span_context().is_valid + finally: + context.detach(token) + + with ThreadPoolExecutor(max_workers=2) as executor: + futures = [executor.submit(consume, suffix) for suffix in ("a", "b")] + for future in futures: + future.result(timeout=10) + + entries = { + span.attributes[GEN_AI_SESSION_ID]: span + for span in _entry_spans(span_exporter) + } + children = { + span.name.removeprefix("embedded-child-"): span + for span in span_exporter.get_finished_spans() + if span.name.startswith("embedded-child-") + } + assert set(entries) == {"baggage-session-a", "baggage-session-b"} + assert set(children) == {"a", "b"} + assert len({entry.context.trace_id for entry in entries.values()}) == 2 + + for suffix in ("a", "b"): + entry = entries[f"baggage-session-{suffix}"] + child = children[suffix] + assert entry.attributes[GEN_AI_USER_ID] == f"baggage-user-{suffix}" + assert child.context.trace_id == entry.context.trace_id + assert child.parent.span_id == entry.context.span_id + + +def test_client_stream_records_ttft_for_empty_tool_call_event_without_capture( + handler, + span_exporter, + monkeypatch, +): + monkeypatch.delenv( + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + raising=False, + ) + + def stream(_message, *, thread_id=None): + del thread_id + yield _Event( + "messages-tuple", + { + "type": "ai", + "id": "tool-decision", + "content": "", + "tool_calls": [{"name": "lookup", "id": "call-1"}], + }, + ) + + result = _ClientStreamWrapper(handler)(stream, _Client(), ("q",), {}) + list(result) + + entry = _entry_spans(span_exporter)[0] + assert entry.attributes["gen_ai.response.time_to_first_token"] >= 0 + assert "gen_ai.output.messages" not in entry.attributes + + +def test_client_stream_generates_and_isolates_correlation_id( + handler, + span_exporter, + monkeypatch, +): + current_trace_id: ContextVar[str | None] = ContextVar( + "fake_deerflow_trace_id", + default=None, + ) + generated_trace_id = "generated-correlation-id" + + trace_context_module = ModuleType("deerflow.trace_context") + trace_context_module.get_current_trace_id = current_trace_id.get + trace_context_module.generate_trace_id = lambda: generated_trace_id + trace_context_module.set_current_trace_id = current_trace_id.set + trace_context_module.reset_current_trace_id = current_trace_id.reset + app_config_module = ModuleType("deerflow.config.app_config") + app_config_module.is_trace_correlation_enabled = lambda _config: True + monkeypatch.setitem( + sys.modules, + "deerflow.trace_context", + trace_context_module, + ) + monkeypatch.setitem( + sys.modules, + "deerflow.config.app_config", + app_config_module, + ) + + observed = {} + + def stream(_message, *, thread_id=None): + del thread_id + observed["inner_trace_id"] = current_trace_id.get() + yield _Event("end", {}) + + client = SimpleNamespace( + _agent_name="research-agent", + _app_config=object(), + ) + assert current_trace_id.get() is None + result = _ClientStreamWrapper(handler)(stream, client, ("q",), {}) + assert current_trace_id.get() is None + list(result) + + entry = _entry_spans(span_exporter)[0] + assert observed["inner_trace_id"] == generated_trace_id + assert entry.attributes["deerflow.trace.id"] == generated_trace_id + assert current_trace_id.get() is None + + +@pytest.mark.parametrize("failure", ["resolve", "bind", "reset"]) +def test_client_trace_correlation_failure_does_not_break_stream( + failure, + handler, + span_exporter, + monkeypatch, + caplog, +): + caplog.set_level( + logging.DEBUG, + logger="opentelemetry.instrumentation.deerflow.internal.patch", + ) + + def fail() -> None: + raise RuntimeError(f"{failure} failed") + + trace_context_module = ModuleType("deerflow.trace_context") + trace_context_module.get_current_trace_id = lambda: None + trace_context_module.generate_trace_id = ( + fail if failure == "resolve" else lambda: "correlation-id" + ) + trace_context_module.set_current_trace_id = ( + (lambda _trace_id: fail()) + if failure == "bind" + else lambda _trace_id: object() + ) + trace_context_module.reset_current_trace_id = ( + (lambda _token: fail()) if failure == "reset" else lambda _token: None + ) + app_config_module = ModuleType("deerflow.config.app_config") + app_config_module.is_trace_correlation_enabled = lambda _config: True + monkeypatch.setitem( + sys.modules, + "deerflow.trace_context", + trace_context_module, + ) + monkeypatch.setitem( + sys.modules, + "deerflow.config.app_config", + app_config_module, + ) + + def stream(_message, *, thread_id=None): + del thread_id + yield _Event("end", {}) + + client = SimpleNamespace( + _agent_name="research-agent", + _app_config=object(), + ) + list(_ClientStreamWrapper(handler)(stream, client, ("q",), {})) + + entry = _entry_spans(span_exporter)[0] + assert entry.status.status_code is StatusCode.UNSET + if failure == "resolve": + assert "deerflow.trace.id" not in entry.attributes + else: + assert entry.attributes["deerflow.trace.id"] == "correlation-id" + expected_log = { + "resolve": "Failed to resolve DeerFlow trace correlation id", + "bind": "Failed to bind DeerFlow trace correlation id", + "reset": "Failed to reset DeerFlow trace context", + }[failure] + assert expected_log in caplog.messages + + +@pytest.mark.asyncio +async def test_gateway_entry_uses_mutated_success_status( + handler, + tracer_provider, + span_exporter, + capture_content, + monkeypatch, +): + def resolve_runtime_user_id(runtime): + return runtime.context.get("user_id", "effective-user") + + deerflow_module = ModuleType("deerflow") + runtime_module = ModuleType("deerflow.runtime") + user_context_module = ModuleType("deerflow.runtime.user_context") + user_context_module.resolve_runtime_user_id = resolve_runtime_user_id + deerflow_module.runtime = runtime_module + runtime_module.user_context = user_context_module + monkeypatch.setitem(sys.modules, "deerflow", deerflow_module) + monkeypatch.setitem(sys.modules, "deerflow.runtime", runtime_module) + monkeypatch.setitem( + sys.modules, + "deerflow.runtime.user_context", + user_context_module, + ) + record = SimpleNamespace( + run_id="run-1", + thread_id="thread-1", + assistant_id="assistant-1", + user_id="stale-record-user", + metadata={"deerflow_trace_id": "trace-1"}, + status="pending", + error=None, + last_ai_message=None, + ) + + async def run_agent(_bridge, _manager, mutable_record, **_kwargs): + tracer = tracer_provider.get_tracer(__name__) + with tracer.start_as_current_span("gateway-child"): + pass + mutable_record.status = "success" + mutable_record.last_ai_message = "finished" + + wrapper = _GatewayRunAgentWrapper(handler) + await wrapper( + run_agent, + None, + (None, None, record), + { + "graph_input": {"messages": [{"role": "user", "content": "go"}]}, + "config": { + "context": {"user_id": "runtime-user"}, + "run_name": "configured-agent", + }, + }, + ) + + entry = _entry_spans(span_exporter)[0] + child = next( + span + for span in span_exporter.get_finished_spans() + if span.name == "gateway-child" + ) + assert child.parent.span_id == entry.context.span_id + assert entry.status.status_code != StatusCode.ERROR + assert entry.attributes[DEERFLOW_RUN_STATUS] == "success" + assert entry.attributes["deerflow.assistant.id"] == "assistant-1" + assert entry.attributes["deerflow.run.id"] == "run-1" + assert entry.attributes["deerflow.trace.id"] == "trace-1" + assert entry.attributes[GEN_AI_SESSION_ID] == "thread-1" + assert entry.attributes[GEN_AI_USER_ID] == "runtime-user" + assert entry.attributes["gen_ai.agent.name"] == "configured-agent" + assert ( + json.loads(entry.attributes["gen_ai.output.messages"])[0]["parts"][0][ + "content" + ] + == "finished" + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("status", "error_type"), + [ + ("error", "DeerFlowRunError"), + ("timeout", "DeerFlowRunTimeout"), + ], +) +async def test_gateway_terminal_status_fails_entry( + status, + error_type, + handler, + span_exporter, +): + record = SimpleNamespace( + run_id="run-1", + thread_id="thread-1", + assistant_id=None, + user_id=None, + metadata={}, + status="running", + error="terminal status", + ) + + async def run_agent(_bridge, _manager, mutable_record, **_kwargs): + mutable_record.status = status + + await _GatewayRunAgentWrapper(handler)( + run_agent, + None, + (None, None, record), + {"graph_input": {}, "config": {}}, + ) + + entry = _entry_spans(span_exporter)[0] + assert entry.status.status_code == StatusCode.ERROR + assert entry.attributes["error.type"] == error_type + assert entry.attributes[DEERFLOW_RUN_STATUS] == status + + +@pytest.mark.asyncio +async def test_gateway_interrupted_status_stops_entry_without_error( + handler, + span_exporter, +): + record = SimpleNamespace( + run_id="run-interrupted", + thread_id="thread-interrupted", + assistant_id=None, + user_id=None, + metadata={}, + status="running", + error=None, + ) + + async def run_agent(_bridge, _manager, mutable_record, **_kwargs): + mutable_record.status = "interrupted" + + await _GatewayRunAgentWrapper(handler)( + run_agent, + None, + (None, None, record), + {"graph_input": {}, "config": {}}, + ) + + entry = _entry_spans(span_exporter)[0] + assert entry.status.status_code == StatusCode.UNSET + assert "error.type" not in entry.attributes + assert entry.attributes[DEERFLOW_RUN_STATUS] == "interrupted" + + +@pytest.mark.asyncio +async def test_gateway_cancellation_interrupts_entry_without_error( + handler, + span_exporter, +): + record = SimpleNamespace( + run_id="run-cancelled", + thread_id="thread-cancelled", + assistant_id=None, + metadata={}, + status="running", + error=None, + ) + + async def run_agent(_bridge, _manager, mutable_record, **_kwargs): + mutable_record.status = "interrupted" + raise asyncio.CancelledError + + with pytest.raises(asyncio.CancelledError): + await _GatewayRunAgentWrapper(handler)( + run_agent, + None, + (None, None, record), + {"graph_input": {}, "config": {}}, + ) + + entry = _entry_spans(span_exporter)[0] + assert entry.status.status_code == StatusCode.UNSET + assert "error.type" not in entry.attributes + assert entry.attributes[DEERFLOW_RUN_STATUS] == "interrupted" + + +@pytest.mark.asyncio +async def test_gateway_reuses_active_host_entry(handler, span_exporter): + record = SimpleNamespace( + run_id="run-hosted", + thread_id="thread-hosted", + assistant_id=None, + metadata={}, + status="running", + error=None, + ) + observed = {} + + async def run_agent(_bridge, _manager, mutable_record, **_kwargs): + observed["span_id"] = ( + trace.get_current_span().get_span_context().span_id + ) + mutable_record.status = "success" + + host = EntryInvocation(session_id="host-session") + handler.start_entry(host) + try: + host_span_id = trace.get_current_span().get_span_context().span_id + await _GatewayRunAgentWrapper(handler)( + run_agent, + None, + (None, None, record), + {"graph_input": {}, "config": {}}, + ) + finally: + handler.stop_entry(host) + + assert observed["span_id"] == host_span_id + assert len(_entry_spans(span_exporter)) == 1 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py new file mode 100644 index 000000000..0759dcbd7 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py @@ -0,0 +1,463 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Instrumentor, version guard, and graph marker tests.""" + +from __future__ import annotations + +import importlib +import inspect +import subprocess +import sys +from importlib.metadata import ( + PackageNotFoundError, +) +from importlib.metadata import ( + version as distribution_version, +) +from types import ModuleType + +import pytest +from packaging.version import Version + +from opentelemetry.instrumentation.deerflow import ( + DeerFlowInstrumentor, + _deerflow_runtime_supported, +) +from opentelemetry.instrumentation.deerflow.internal.constants import ( + AGENT_FRAMEWORK_ATTR, + AGENT_STEP_NODE_ATTR, + GATEWAY_RUN_AGENT_ALIASES, +) +from opentelemetry.instrumentation.deerflow.internal.patch import ( + _create_agent_alias_wrapper, + instrument_deerflow, + mark_deerflow_graph, + uninstrument_deerflow, +) +from opentelemetry.instrumentation.deerflow.package import _instruments + + +class _Graph: + pass + + +def _wrapper_modules(value): + modules = [] + seen = set() + while hasattr(value, "__wrapped__") and id(value) not in seen: + seen.add(id(value)) + wrapper = getattr(value, "_self_wrapper", None) + modules.append(getattr(wrapper, "__module__", None)) + value = value.__wrapped__ + return modules + + +def test_has_no_pypi_target_library_dependency(): + assert _instruments == () + assert DeerFlowInstrumentor().instrumentation_dependencies() == () + + +def test_official_source_distribution_and_gateway_signature_are_supported(): + assert Version("2") <= Version(distribution_version("deerflow-harness")) + assert Version(distribution_version("deerflow-harness")) < Version("3") + assert _deerflow_runtime_supported() is True + + run_agent = importlib.import_module( + "deerflow.runtime.runs.worker" + ).run_agent + parameters = inspect.signature(inspect.unwrap(run_agent)).parameters + assert list(parameters)[:3] == ["bridge", "run_manager", "record"] + assert parameters["graph_input"].kind is inspect.Parameter.KEYWORD_ONLY + assert parameters["config"].kind is inspect.Parameter.KEYWORD_ONLY + + +def test_probe_first_cold_start_wraps_each_deerflow_alias_once(): + script = """ +import importlib +import sys + +assert not any(name == "deerflow" or name.startswith("deerflow.") for name in sys.modules) + +from opentelemetry.instrumentation.deerflow import DeerFlowInstrumentor +from opentelemetry.instrumentation.deerflow.internal.constants import ( + CREATE_AGENT_ALIASES, + GATEWAY_RUN_AGENT_ALIASES, +) + + +def wrapper_depth(value): + depth = 0 + seen = set() + while hasattr(value, "__wrapped__") and id(value) not in seen: + seen.add(id(value)) + value = value.__wrapped__ + depth += 1 + return depth + + +def wrapper_modules(value): + modules = [] + seen = set() + while hasattr(value, "__wrapped__") and id(value) not in seen: + seen.add(id(value)) + wrapper = getattr(value, "_self_wrapper", None) + modules.append(getattr(wrapper, "__module__", None)) + value = value.__wrapped__ + return modules + + +instrumentor = DeerFlowInstrumentor() +instrumentor.instrument() +assert instrumentor.is_instrumented_by_opentelemetry + +aliases = (*CREATE_AGENT_ALIASES, *GATEWAY_RUN_AGENT_ALIASES) +for module_name, target in aliases: + module = importlib.import_module(module_name) + value = getattr(module, target) + modules = wrapper_modules(value) + assert modules.count( + "opentelemetry.instrumentation.deerflow.internal.patch" + ) == 1, (module_name, target, modules) + expected_depth = 2 if (module_name, target) in CREATE_AGENT_ALIASES else 1 + assert wrapper_depth(value) == expected_depth, (module_name, target, modules) + +instrumentor.uninstrument() +for module_name, target in aliases: + module = importlib.import_module(module_name) + assert wrapper_depth(getattr(module, target)) == 0, (module_name, target) +""" + result = subprocess.run( # noqa: S603 + [sys.executable, "-c", script], + check=False, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stdout + result.stderr + + +@pytest.mark.parametrize( + ("installed_version", "supported"), + [ + ("1.9.9", False), + ("2.0.0", True), + ("2.1.0", True), + ("3.0.0", False), + ], +) +def test_runtime_version_guard( + installed_version, + supported, + monkeypatch, +): + real_import = importlib.import_module + + def import_module(name): + if name == "deerflow": + return object() + return real_import(name) + + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.importlib.import_module", + import_module, + ) + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.version", + lambda _distribution: installed_version, + ) + + assert _deerflow_runtime_supported() is supported + + +def test_runtime_guard_silently_skips_failed_deerflow_import(monkeypatch): + missing_dependency = ModuleNotFoundError("missing DeerFlow dependency") + missing_dependency.name = "deerflow_optional_dependency" + + def fail_import(_name): + raise missing_dependency + + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.importlib.import_module", + fail_import, + ) + + assert _deerflow_runtime_supported() is False + + +def test_runtime_guard_silently_skips_missing_distribution_metadata( + monkeypatch, +): + real_import = importlib.import_module + + def import_module(name): + if name == "deerflow": + return object() + return real_import(name) + + def missing_distribution(_distribution): + raise PackageNotFoundError + + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.importlib.import_module", + import_module, + ) + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.version", + missing_distribution, + ) + + assert _deerflow_runtime_supported() is False + + +def test_instrument_rolls_back_started_dependency_on_setup_error(monkeypatch): + class StartedDependency: + def __init__(self): + self.uninstrument_calls = 0 + + def uninstrument(self): + self.uninstrument_calls += 1 + + dependency = StartedDependency() + dependency_calls = 0 + deerflow_uninstrument_calls = 0 + + def instrument_dependency(*_args, **_kwargs): + nonlocal dependency_calls + dependency_calls += 1 + if dependency_calls == 1: + return dependency + raise RuntimeError("dependency setup failed") + + def uninstrument_patches(): + nonlocal deerflow_uninstrument_calls + deerflow_uninstrument_calls += 1 + + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow._deerflow_runtime_supported", + lambda: True, + ) + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow._instrument_dependency", + instrument_dependency, + ) + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.uninstrument_deerflow", + uninstrument_patches, + ) + + instrumentor = DeerFlowInstrumentor() + with pytest.raises(RuntimeError, match="dependency setup failed"): + instrumentor._instrument() + + assert dependency.uninstrument_calls == 1 + assert deerflow_uninstrument_calls == 1 + assert instrumentor._dependency_instrumentors == [] + assert instrumentor._deerflow_patched is False + + +def test_uninstrument_cleans_owned_dependencies_and_aliases(monkeypatch): + events = [] + + langchain_dependency_type = type( + "LangChainInstrumentor", + (), + { + "__module__": "opentelemetry.instrumentation.langchain", + "uninstrument": lambda self: events.append("langchain"), + }, + ) + other_dependency_type = type( + "OtherInstrumentor", + (), + {"uninstrument": lambda self: events.append("other")}, + ) + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.uninstrument_deerflow", + lambda: events.append("deerflow"), + ) + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.remove_owned_langchain_alias_wrappers", + lambda: events.append("aliases"), + ) + instrumentor = DeerFlowInstrumentor() + instrumentor._deerflow_patched = True + instrumentor._dependency_instrumentors = [ + langchain_dependency_type(), + other_dependency_type(), + ] + + instrumentor._uninstrument() + + assert events == ["deerflow", "other", "langchain", "aliases"] + assert instrumentor._deerflow_patched is False + assert instrumentor._dependency_instrumentors == [] + + +def test_uninstrument_continues_when_dependency_cleanup_fails(monkeypatch): + events = [] + + class FailingDependency: + def uninstrument(self): + events.append("failing") + raise RuntimeError("cleanup failed") + + class HealthyDependency: + def uninstrument(self): + events.append("healthy") + + monkeypatch.setattr( + "opentelemetry.instrumentation.deerflow.uninstrument_deerflow", + lambda: events.append("deerflow"), + ) + instrumentor = DeerFlowInstrumentor() + instrumentor._deerflow_patched = True + instrumentor._dependency_instrumentors = [ + HealthyDependency(), + FailingDependency(), + ] + + instrumentor._uninstrument() + + assert events == ["deerflow", "failing", "healthy"] + assert instrumentor._dependency_instrumentors == [] + + +def test_create_agent_alias_marks_only_deerflow_semantics(): + graph = _Graph() + graph._loongsuite_react_agent = True + result = _create_agent_alias_wrapper( + lambda: graph, + None, + (), + {}, + ) + + assert result is graph + assert getattr(graph, AGENT_FRAMEWORK_ATTR) == "deerflow" + assert getattr(graph, AGENT_STEP_NODE_ATTR) == "model" + assert not hasattr(graph, "_loongsuite_react_agent") + assert not hasattr(graph, "_loongsuite_deepagents_agent") + + uninstrument_deerflow() + assert not hasattr(graph, AGENT_FRAMEWORK_ATTR) + assert not hasattr(graph, AGENT_STEP_NODE_ATTR) + assert graph._loongsuite_react_agent is True + + +def test_uninstrument_restores_previous_graph_semantics(): + graph = _Graph() + setattr(graph, AGENT_FRAMEWORK_ATTR, "custom-framework") + setattr(graph, AGENT_STEP_NODE_ATTR, "custom-step") + + mark_deerflow_graph(graph) + assert getattr(graph, AGENT_FRAMEWORK_ATTR) == "deerflow" + assert getattr(graph, AGENT_STEP_NODE_ATTR) == "model" + + uninstrument_deerflow() + assert getattr(graph, AGENT_FRAMEWORK_ATTR) == "custom-framework" + assert getattr(graph, AGENT_STEP_NODE_ATTR) == "custom-step" + + +def test_gateway_aliases_are_wrapped_and_restored( + handler, + monkeypatch, +): + real_aliases = [] + for module_name, target in GATEWAY_RUN_AGENT_ALIASES: + module = importlib.import_module(module_name) + real_aliases.append((module, target, getattr(module, target))) + + async def service_run_agent(*_args, **_kwargs): + return None + + app_module = ModuleType("app") + app_module.__path__ = [] + gateway_module = ModuleType("app.gateway") + gateway_module.__path__ = [] + services_module = ModuleType("app.gateway.services") + services_module.run_agent = service_run_agent + app_module.gateway = gateway_module + gateway_module.services = services_module + monkeypatch.setitem(sys.modules, "app", app_module) + monkeypatch.setitem(sys.modules, "app.gateway", gateway_module) + monkeypatch.setitem( + sys.modules, + "app.gateway.services", + services_module, + ) + + try: + assert instrument_deerflow(handler) is True + for module, target, original in real_aliases: + wrapped = getattr(module, target) + assert wrapped is not original + assert inspect.unwrap(wrapped) is original + + assert services_module.run_agent is not service_run_agent + assert inspect.unwrap(services_module.run_agent) is service_run_agent + finally: + uninstrument_deerflow() + + for module, target, original in real_aliases: + assert getattr(module, target) is original + assert services_module.run_agent is service_run_agent + + +def test_direct_instrumentation_is_idempotent(handler): + module = importlib.import_module("deerflow.agents.factory") + target = "create_agent" + + try: + assert instrument_deerflow(handler) is True + first_modules = _wrapper_modules(getattr(module, target)) + + assert instrument_deerflow(handler) is False + second_modules = _wrapper_modules(getattr(module, target)) + + assert first_modules == second_modules + assert ( + second_modules.count( + "opentelemetry.instrumentation.deerflow.internal.patch" + ) + == 1 + ) + finally: + uninstrument_deerflow() + + +def test_late_loaded_gateway_alias_is_restored(handler, monkeypatch): + monkeypatch.delitem(sys.modules, "app.gateway.services", raising=False) + runtime_module = importlib.import_module("deerflow.runtime") + original = runtime_module.run_agent + + try: + assert instrument_deerflow(handler) is True + late_alias = runtime_module.run_agent + assert late_alias is not original + + services_module = ModuleType("app.gateway.services") + services_module.run_agent = late_alias + monkeypatch.setitem( + sys.modules, + "app.gateway.services", + services_module, + ) + + uninstrument_deerflow() + + assert runtime_module.run_agent is original + assert services_module.run_agent is original + finally: + uninstrument_deerflow() diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py new file mode 100644 index 000000000..08feeafc1 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py @@ -0,0 +1,491 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Runtime matrix against the pinned official DeerFlow 2.x source.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any, Optional + +import pytest +from pydantic import PrivateAttr + +from opentelemetry.instrumentation.deerflow import DeerFlowInstrumentor +from opentelemetry.instrumentation.langchain import LangChainInstrumentor +from opentelemetry.instrumentation.langgraph import LangGraphInstrumentor +from opentelemetry.trace import StatusCode + +deerflow = pytest.importorskip("deerflow") + +from deerflow.agents.factory import create_deerflow_agent # noqa: E402 +from langchain_core.callbacks import CallbackManagerForLLMRun # noqa: E402 +from langchain_core.language_models.chat_models import ( # noqa: E402 + BaseChatModel, +) +from langchain_core.messages import AIMessage, BaseMessage # noqa: E402 +from langchain_core.outputs import ChatGeneration, ChatResult # noqa: E402 +from langchain_core.tools import tool # noqa: E402 +from langgraph.errors import GraphRecursionError # noqa: E402 + + +def _final_message(content: str = "finished") -> AIMessage: + return AIMessage( + content=content, + response_metadata={"finish_reason": "stop"}, + ) + + +def _tool_call_message( + tool_name: str, + *, + call_id: str = "call-1", + args: dict[str, Any] | None = None, +) -> AIMessage: + return AIMessage( + content="", + tool_calls=[ + { + "name": tool_name, + "args": args or {}, + "id": call_id, + "type": "tool_call", + } + ], + response_metadata={"finish_reason": "tool_calls"}, + ) + + +class _ScriptedChatModel(BaseChatModel): + """Small deterministic model supporting sync, stream, and async graphs.""" + + _responses: list[AIMessage | BaseException] = PrivateAttr() + _repeat_tool_name: str | None = PrivateAttr() + _calls: int = PrivateAttr(default=0) + + def __init__( + self, + responses: Sequence[AIMessage | BaseException] = (), + *, + repeat_tool_name: str | None = None, + ) -> None: + super().__init__() + self._responses = list(responses) + self._repeat_tool_name = repeat_tool_name + + @property + def _llm_type(self) -> str: + return "fake-deerflow" + + @property + def _identifying_params(self) -> dict: + return {} + + def bind_tools(self, tools, **kwargs): + del tools, kwargs + return self + + def _next_response(self) -> AIMessage: + self._calls += 1 + if self._responses: + response = self._responses.pop(0) + if isinstance(response, BaseException): + raise response + return response + if self._repeat_tool_name is not None: + return _tool_call_message( + self._repeat_tool_name, + call_id=f"call-{self._calls}", + ) + return _final_message() + + def _generate( + self, + messages: list[BaseMessage], + stop: Optional[list[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + del messages, stop, run_manager, kwargs + message = self._next_response() + finish_reason = message.response_metadata.get( + "finish_reason", + "stop", + ) + return ChatResult( + generations=[ + ChatGeneration( + message=message, + generation_info={"finish_reason": finish_reason}, + ) + ], + llm_output={"model_name": "fake-deerflow"}, + ) + + +class _ProviderCallbackRecorder: + """Record provider-style callbacks without contacting either backend.""" + + def __init__(self) -> None: + self.events: list[tuple[str, dict[str, Any] | None]] = [] + # LangGraph recognizes LangSmith tracers by type and reads this map. + self.run_map: dict[str, Any] = {} + + def copy_with_metadata_defaults(self, **_kwargs: Any): + """Keep provider callback copies local and network-free in tests.""" + return self + + def _record(self, event: str, kwargs: dict[str, Any]) -> None: + metadata = kwargs.get("metadata") + self.events.append( + (event, metadata if isinstance(metadata, dict) else None) + ) + + def on_chain_start(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("chain_start", kwargs) + + def on_chain_end(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("chain_end", kwargs) + + def on_chain_error(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("chain_error", kwargs) + + def on_chat_model_start(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("chat_model_start", kwargs) + + def on_llm_start(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("llm_start", kwargs) + + def on_llm_end(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("llm_end", kwargs) + + def on_llm_error(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("llm_error", kwargs) + + def on_tool_start(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("tool_start", kwargs) + + def on_tool_end(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("tool_end", kwargs) + + def on_tool_error(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("tool_error", kwargs) + + def on_agent_action(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("agent_action", kwargs) + + def on_agent_finish(self, *args: Any, **kwargs: Any) -> None: + del args + self._record("agent_finish", kwargs) + + +@tool +def lookup(query: str) -> str: + """Look up a deterministic test value.""" + return f"result:{query}" + + +@tool +def fail_tool() -> str: + """Fail deterministically for lifecycle verification.""" + raise ValueError("tool failed") + + +@tool +def ping() -> str: + """Return a deterministic value for recursion tests.""" + return "pong" + + +@pytest.fixture +def instrumented_deerflow(tracer_provider): + instrumentor = DeerFlowInstrumentor() + instrumentor.instrument(tracer_provider=tracer_provider) + try: + yield + finally: + if instrumentor.is_instrumented_by_opentelemetry: + instrumentor.uninstrument() + + +def _spans_of_kind(spans, kind: str): + return [ + span + for span in spans + if span.attributes.get("gen_ai.span.kind") == kind + ] + + +def _is_descendant(child, ancestor, spans) -> bool: + spans_by_id = {span.context.span_id: span for span in spans} + parent = child.parent + while parent is not None: + if parent.span_id == ancestor.context.span_id: + return True + parent_span = spans_by_id.get(parent.span_id) + parent = parent_span.parent if parent_span is not None else None + return False + + +def _assert_direct_sdk_tree(spans, *, model_decisions: int) -> None: + assert _spans_of_kind(spans, "ENTRY") == [] + + agents = _spans_of_kind(spans, "AGENT") + steps = _spans_of_kind(spans, "STEP") + llms = _spans_of_kind(spans, "LLM") + assert len(agents) == 1 + assert len(steps) == model_decisions + assert len(llms) == model_decisions + + agent = agents[0] + assert agent.parent is None + assert agent.attributes["gen_ai.framework"] == "deerflow" + assert agent.attributes["gen_ai.agent.name"] == "sdk-agent" + assert all(step.parent.span_id == agent.context.span_id for step in steps) + assert all( + sum(_is_descendant(llm, step, spans) for step in steps) == 1 + for llm in llms + ) + + +def _new_graph(model, tools=None): + return create_deerflow_agent( + model=model, + tools=tools, + middleware=[], + name="sdk-agent", + ) + + +@pytest.mark.parametrize("operation", ["invoke", "stream"]) +def test_direct_sdk_sync_operations_have_agent_step_without_entry( + operation, + instrumented_deerflow, + span_exporter, +): + graph = _new_graph(_ScriptedChatModel([_final_message()])) + + if operation == "invoke": + graph.invoke({"messages": [{"role": "user", "content": "hello"}]}) + else: + list( + graph.stream({"messages": [{"role": "user", "content": "hello"}]}) + ) + + assert getattr(graph, "_loongsuite_agent_framework") == "deerflow" + assert getattr(graph, "_loongsuite_agent_step_node") == "model" + _assert_direct_sdk_tree( + span_exporter.get_finished_spans(), + model_decisions=1, + ) + + +def test_langsmith_and_langfuse_callbacks_keep_user_config( + instrumented_deerflow, + span_exporter, +): + langfuse_langchain = pytest.importorskip("langfuse.langchain") + from langchain_core.tracers.langchain import ( # noqa: PLC0415 + LangChainTracer, + ) + + class _LangSmithRecorder(_ProviderCallbackRecorder, LangChainTracer): + pass + + class _LangfuseRecorder( + _ProviderCallbackRecorder, + langfuse_langchain.CallbackHandler, + ): + pass + + langsmith_callback = _LangSmithRecorder() + langfuse_callback = _LangfuseRecorder() + callbacks = [langsmith_callback, langfuse_callback] + metadata = { + "customer": "kept", + "langfuse_trace_name": "deerflow-coexistence", + } + config = {"callbacks": callbacks, "metadata": metadata} + graph = _new_graph(_ScriptedChatModel([_final_message()])) + + graph.invoke( + {"messages": [{"role": "user", "content": "hello"}]}, + config=config, + ) + + assert config["callbacks"] is callbacks + assert config["metadata"] is metadata + assert "_loongsuite_agent_framework" not in metadata + assert "_loongsuite_agent_step_node" not in metadata + for callback in callbacks: + assert callback.events + assert any( + event_metadata is not None + and event_metadata.get("customer") == "kept" + for _, event_metadata in callback.events + ) + _assert_direct_sdk_tree( + span_exporter.get_finished_spans(), + model_decisions=1, + ) + + +def test_dependency_instrumentors_do_not_duplicate_agent_or_step( + tracer_provider, + span_exporter, +): + langchain_instrumentor = LangChainInstrumentor() + langgraph_instrumentor = LangGraphInstrumentor() + deerflow_instrumentor = DeerFlowInstrumentor() + langchain_instrumentor.instrument(tracer_provider=tracer_provider) + langgraph_instrumentor.instrument() + deerflow_instrumentor.instrument(tracer_provider=tracer_provider) + try: + graph = _new_graph(_ScriptedChatModel([_final_message()])) + graph.invoke({"messages": [{"role": "user", "content": "hello"}]}) + + spans = span_exporter.get_finished_spans() + assert len(_spans_of_kind(spans, "AGENT")) == 1 + assert len(_spans_of_kind(spans, "STEP")) == 1 + assert len(_spans_of_kind(spans, "LLM")) == 1 + finally: + deerflow_instrumentor.uninstrument() + langgraph_instrumentor.uninstrument() + langchain_instrumentor.uninstrument() + + +@pytest.mark.asyncio +async def test_direct_sdk_ainvoke_has_agent_step_without_entry( + instrumented_deerflow, + span_exporter, +): + graph = _new_graph(_ScriptedChatModel([_final_message()])) + + await graph.ainvoke({"messages": [{"role": "user", "content": "hello"}]}) + + _assert_direct_sdk_tree( + span_exporter.get_finished_spans(), + model_decisions=1, + ) + + +def test_two_decision_tool_loop_has_exact_steps_and_finish_reasons( + instrumented_deerflow, + span_exporter, +): + model = _ScriptedChatModel( + [ + _tool_call_message( + "lookup", + args={"query": "deerflow"}, + ), + _final_message("done"), + ] + ) + graph = _new_graph(model, [lookup]) + + graph.invoke({"messages": [{"role": "user", "content": "research"}]}) + + spans = span_exporter.get_finished_spans() + _assert_direct_sdk_tree(spans, model_decisions=2) + steps = _spans_of_kind(spans, "STEP") + tools = _spans_of_kind(spans, "TOOL") + assert [ + step.attributes.get("gen_ai.react.finish_reason") for step in steps + ] == ["tool_calls", "stop"] + assert len(tools) == 1 + assert tools[0].attributes["gen_ai.tool.name"] == "lookup" + + +def test_model_exception_closes_step_and_agent_as_error( + instrumented_deerflow, + span_exporter, +): + graph = _new_graph(_ScriptedChatModel([RuntimeError("model failed")])) + + with pytest.raises(RuntimeError, match="model failed"): + graph.invoke({"messages": [{"role": "user", "content": "fail"}]}) + + spans = span_exporter.get_finished_spans() + _assert_direct_sdk_tree(spans, model_decisions=1) + assert _spans_of_kind(spans, "LLM")[0].status.status_code == ( + StatusCode.ERROR + ) + assert _spans_of_kind(spans, "STEP")[0].status.status_code == ( + StatusCode.ERROR + ) + assert _spans_of_kind(spans, "AGENT")[0].status.status_code == ( + StatusCode.ERROR + ) + + +def test_tool_exception_closes_tool_step_and_agent_as_error( + instrumented_deerflow, + span_exporter, +): + graph = _new_graph( + _ScriptedChatModel([_tool_call_message("fail_tool")]), + [fail_tool], + ) + + with pytest.raises(ValueError, match="tool failed"): + graph.invoke({"messages": [{"role": "user", "content": "fail"}]}) + + spans = span_exporter.get_finished_spans() + _assert_direct_sdk_tree(spans, model_decisions=1) + assert _spans_of_kind(spans, "TOOL")[0].status.status_code == ( + StatusCode.ERROR + ) + assert _spans_of_kind(spans, "STEP")[0].status.status_code == ( + StatusCode.ERROR + ) + assert _spans_of_kind(spans, "AGENT")[0].status.status_code == ( + StatusCode.ERROR + ) + + +def test_recursion_limit_closes_last_step_and_agent_as_error( + instrumented_deerflow, + span_exporter, +): + graph = _new_graph( + _ScriptedChatModel(repeat_tool_name="ping"), + [ping], + ) + + with pytest.raises(GraphRecursionError): + graph.invoke( + {"messages": [{"role": "user", "content": "loop"}]}, + config={"recursion_limit": 3}, + ) + + spans = span_exporter.get_finished_spans() + _assert_direct_sdk_tree(spans, model_decisions=2) + steps = _spans_of_kind(spans, "STEP") + assert steps[0].attributes["gen_ai.react.finish_reason"] == "tool_calls" + assert steps[-1].status.status_code == StatusCode.ERROR + assert _spans_of_kind(spans, "AGENT")[0].status.status_code == ( + StatusCode.ERROR + ) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_subagents.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_subagents.py new file mode 100644 index 000000000..428b6d290 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_subagents.py @@ -0,0 +1,373 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Real DeerFlow ``task`` and subagent-executor trace regressions.""" + +from __future__ import annotations + +import asyncio +import importlib +from collections.abc import Sequence +from types import SimpleNamespace +from typing import Any, Optional + +import pytest +from pydantic import PrivateAttr + +from opentelemetry.instrumentation.deerflow import DeerFlowInstrumentor + +deerflow = pytest.importorskip("deerflow") + +from deerflow.agents.factory import create_deerflow_agent # noqa: E402 +from deerflow.subagents.config import SubagentConfig # noqa: E402 +from deerflow.tools.builtins.task_tool import task_tool # noqa: E402 +from langchain_core.callbacks import CallbackManagerForLLMRun # noqa: E402 +from langchain_core.language_models.chat_models import ( # noqa: E402 + BaseChatModel, +) +from langchain_core.messages import AIMessage, BaseMessage # noqa: E402 +from langchain_core.outputs import ChatGeneration, ChatResult # noqa: E402 + + +def _final_message(content: str) -> AIMessage: + return AIMessage( + content=content, + response_metadata={"finish_reason": "stop"}, + ) + + +def _task_calls_message(*subagent_names: str) -> AIMessage: + return AIMessage( + content="", + tool_calls=[ + { + "name": "task", + "args": { + "description": f"delegate {name}", + "prompt": f"research with {name}", + "subagent_type": name, + }, + "id": f"call-{name}", + "type": "tool_call", + } + for name in subagent_names + ], + response_metadata={"finish_reason": "tool_calls"}, + ) + + +class _ScriptedChatModel(BaseChatModel): + """Deterministic model used by the real lead and subagent graphs.""" + + _responses: list[AIMessage] = PrivateAttr() + + def __init__(self, responses: Sequence[AIMessage]) -> None: + super().__init__() + self._responses = list(responses) + + @property + def _llm_type(self) -> str: + return "fake-deerflow-subagent" + + @property + def _identifying_params(self) -> dict: + return {} + + def bind_tools(self, tools, **kwargs): + del tools, kwargs + return self + + def _generate( + self, + messages: list[BaseMessage], + stop: Optional[list[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + del messages, stop, run_manager, kwargs + message = self._responses.pop(0) + finish_reason = message.response_metadata["finish_reason"] + return ChatResult( + generations=[ + ChatGeneration( + message=message, + generation_info={"finish_reason": finish_reason}, + ) + ], + llm_output={"model_name": "fake-deerflow-subagent"}, + ) + + +class _FastAsyncio: + """Proxy only DeerFlow task polling sleep; keep global asyncio untouched.""" + + def __getattr__(self, name: str) -> Any: + return getattr(asyncio, name) + + @staticmethod + async def sleep(_delay: float) -> None: + # The real task tool polls every five seconds. Keep a small real wait + # so the official scheduler thread and isolated event loop get CPU, + # while making the regression deterministic and fast. + await asyncio.sleep(0.05) + + +@pytest.fixture +def instrumented_deerflow(tracer_provider): + instrumentor = DeerFlowInstrumentor() + instrumentor.instrument(tracer_provider=tracer_provider) + try: + yield + finally: + if instrumentor.is_instrumented_by_opentelemetry: + instrumentor.uninstrument() + + +@pytest.fixture +def real_subagent_runtime(monkeypatch): + """Keep official task/executor concurrency, replacing only external IO.""" + task_module = importlib.import_module("deerflow.tools.builtins.task_tool") + executor_module = importlib.import_module("deerflow.subagents.executor") + tools_module = importlib.import_module("deerflow.tools") + middleware_module = importlib.import_module( + "deerflow.agents.middlewares.tool_error_handling_middleware" + ) + + app_config = SimpleNamespace( + tool_search=SimpleNamespace( + enabled=False, + auto_promote_top_k=0, + ) + ) + models: dict[str, BaseChatModel] = {} + + def get_subagent_config( + name: str, + *_args: Any, + **_kwargs: Any, + ) -> SubagentConfig: + return SubagentConfig( + name=name, + description=f"test subagent {name}", + model=name, + skills=[], + max_turns=10, + timeout_seconds=10, + ) + + def create_chat_model(name: str, **_kwargs: Any) -> BaseChatModel: + return models[name] + + monkeypatch.setattr(task_module, "asyncio", _FastAsyncio()) + monkeypatch.setattr( + task_module, + "get_available_subagent_names", + lambda *_args, **_kwargs: list(models), + ) + monkeypatch.setattr( + task_module, + "get_subagent_config", + get_subagent_config, + ) + monkeypatch.setattr( + task_module, + "resolve_subagent_model_name", + lambda config, _parent, **_kwargs: config.model, + ) + monkeypatch.setattr( + task_module, + "resolve_runtime_user_id", + lambda runtime: (runtime.context or {}).get("user_id"), + ) + monkeypatch.setattr( + task_module, + "_token_usage_cache_enabled", + lambda _config: False, + ) + monkeypatch.setattr(task_module, "get_app_config", lambda: app_config) + monkeypatch.setattr( + tools_module, + "get_available_tools", + lambda **_kwargs: [], + ) + monkeypatch.setattr( + executor_module, + "get_app_config", + lambda: app_config, + ) + monkeypatch.setattr( + executor_module, + "create_chat_model", + create_chat_model, + ) + monkeypatch.setattr( + executor_module, + "build_tracing_callbacks", + lambda: [], + ) + monkeypatch.setattr( + middleware_module, + "build_subagent_runtime_middlewares", + lambda **_kwargs: [], + ) + + yield models + + # A failed assertion must not leave official background-task state behind. + background_tasks = getattr(executor_module, "_background_tasks", {}) + background_tasks.clear() + + +def _spans_of_kind(spans, kind: str): + return [ + span + for span in spans + if span.attributes.get("gen_ai.span.kind") == kind + ] + + +def _is_descendant(child, ancestor, spans) -> bool: + spans_by_id = {span.context.span_id: span for span in spans} + parent = child.parent + while parent is not None: + if parent.span_id == ancestor.context.span_id: + return True + parent_span = spans_by_id.get(parent.span_id) + parent = parent_span.parent if parent_span is not None else None + return False + + +def _assert_subagent_tree(spans, *subagent_names: str) -> None: + assert _spans_of_kind(spans, "ENTRY") == [] + + agents = _spans_of_kind(spans, "AGENT") + tools = _spans_of_kind(spans, "TOOL") + steps = _spans_of_kind(spans, "STEP") + llms = _spans_of_kind(spans, "LLM") + + expected_agent_names = { + "lead-agent", + *(f"subagent:{name}" for name in subagent_names), + } + assert {span.attributes["gen_ai.agent.name"] for span in agents} == ( + expected_agent_names + ) + assert len(agents) == len(expected_agent_names) + assert len(tools) == len(subagent_names) + assert all(tool.attributes["gen_ai.tool.name"] == "task" for tool in tools) + assert len(steps) == 2 + len(subagent_names) + assert len(llms) == 2 + len(subagent_names) + + trace_ids = {span.context.trace_id for span in spans} + assert len(trace_ids) == 1 + + lead = next( + span + for span in agents + if span.attributes["gen_ai.agent.name"] == "lead-agent" + ) + assert lead.parent is None + + subagents = [span for span in agents if span is not lead] + tool_ids = {span.context.span_id for span in tools} + assert {span.parent.span_id for span in subagents} == tool_ids + assert all(_is_descendant(tool, lead, spans) for tool in tools) + + for subagent in subagents: + child_steps = [ + step + for step in steps + if step.parent.span_id == subagent.context.span_id + ] + assert len(child_steps) == 1 + assert ( + sum(_is_descendant(llm, child_steps[0], spans) for llm in llms) + == 1 + ) + + +@pytest.mark.asyncio +async def test_official_task_runs_one_real_subagent_without_extra_entry( + instrumented_deerflow, + real_subagent_runtime, + span_exporter, +): + real_subagent_runtime["research"] = _ScriptedChatModel( + [_final_message("subagent result")] + ) + lead_model = _ScriptedChatModel( + [ + _task_calls_message("research"), + _final_message("lead result"), + ] + ) + graph = create_deerflow_agent( + model=lead_model, + tools=[task_tool], + middleware=[], + name="lead-agent", + ) + + await graph.ainvoke( + {"messages": [{"role": "user", "content": "delegate"}]}, + config={"metadata": {"model_name": "lead-model"}}, + context={"thread_id": "thread-one", "user_id": "user-one"}, + ) + + _assert_subagent_tree( + span_exporter.get_finished_spans(), + "research", + ) + + +@pytest.mark.asyncio +async def test_official_task_keeps_two_concurrent_subagents_on_distinct_parents( + instrumented_deerflow, + real_subagent_runtime, + span_exporter, +): + real_subagent_runtime.update( + { + "research-one": _ScriptedChatModel( + [_final_message("first result")] + ), + "research-two": _ScriptedChatModel( + [_final_message("second result")] + ), + } + ) + lead_model = _ScriptedChatModel( + [ + _task_calls_message("research-one", "research-two"), + _final_message("combined result"), + ] + ) + graph = create_deerflow_agent( + model=lead_model, + tools=[task_tool], + middleware=[], + name="lead-agent", + ) + + await graph.ainvoke( + {"messages": [{"role": "user", "content": "parallel delegate"}]}, + config={"metadata": {"model_name": "lead-model"}}, + context={"thread_id": "thread-two", "user_id": "user-two"}, + ) + + _assert_subagent_tree( + span_exporter.get_finished_spans(), + "research-one", + "research-two", + ) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/CHANGELOG.md index 2edd2506e..43e1dd1e6 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Recognize opt-in framework and decision-node metadata from LangGraph-based + agent harnesses, allowing adapters such as DeerFlow to define their `STEP` + boundary without changing existing LangGraph or DeepAgents detection. + ## Version 0.7.0 (2026-07-03) ### Added diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_semantics.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_semantics.py new file mode 100644 index 000000000..1c7076d43 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_semantics.py @@ -0,0 +1,36 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Internal metadata contract for LangGraph-based agent harnesses.""" + +from __future__ import annotations + +from typing import Any + +AGENT_FRAMEWORK_METADATA_KEY = "_loongsuite_agent_framework" +AGENT_STEP_NODE_METADATA_KEY = "_loongsuite_agent_step_node" +DEERFLOW_FRAMEWORK = "deerflow" + + +def get_agent_semantics(run: Any) -> tuple[str, str] | None: + """Return validated framework and decision-node metadata for a run.""" + + metadata = getattr(run, "metadata", None) or {} + framework = metadata.get(AGENT_FRAMEWORK_METADATA_KEY) + step_node = metadata.get(AGENT_STEP_NODE_METADATA_KEY) + if not isinstance(framework, str) or not framework.strip(): + return None + if not isinstance(step_node, str) or not step_node.strip(): + return None + return framework.strip(), step_node.strip() diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py index aae2dd507..b94337b45 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py @@ -48,8 +48,13 @@ from langchain_core.tracers.base import BaseTracer from langchain_core.tracers.schemas import Run +from opentelemetry import baggage from opentelemetry import context as otel_context from opentelemetry.context import Context +from opentelemetry.instrumentation.langchain.internal._agent_semantics import ( + DEERFLOW_FRAMEWORK, + get_agent_semantics, +) from opentelemetry.instrumentation.langchain.internal._utils import ( DEEPAGENTS_REACT_STEP_NODE, LANGGRAPH_REACT_STEP_NODE, @@ -127,6 +132,10 @@ class _RunData: inside_langgraph_react: bool = False is_deepagents_react: bool = False inside_deepagents_react: bool = False + agent_framework: str | None = None + agent_step_node: str | None = None + inside_agent_framework: str | None = None + inside_agent_step_node: str | None = None deepagents_skills_by_path: dict[str, dict[str, Any]] = field( default_factory=dict ) @@ -354,6 +363,8 @@ def _on_chain_start(self, run: Run) -> None: try: if _is_agent_run(run): self._start_agent(run) + elif get_agent_semantics(run) is not None: + self._handle_semantic_agent_chain_start(run) elif _has_langgraph_react_metadata( run ) or _has_deepagents_metadata(run): @@ -363,6 +374,24 @@ def _on_chain_start(self, run: Run) -> None: except Exception: logger.debug("Failed to start Chain/Agent span", exc_info=True) + def _handle_semantic_agent_chain_start(self, run: Run) -> None: + """Route an opt-in graph carrying explicit agent semantics.""" + + parent_id = getattr(run, "parent_run_id", None) + with self._lock: + parent_rd = self._runs.get(parent_id) if parent_id else None + + inside = parent_rd is not None and ( + parent_rd.agent_framework is not None + or parent_rd.inside_agent_framework is not None + ) + if inside: + if parent_rd is not None and parent_rd.agent_framework is not None: + self._maybe_enter_semantic_agent_step(run) + self._start_chain(run) + else: + self._start_agent(run) + def _handle_react_chain_start(self, run: Run) -> None: """Route a chain start that carries LoongSuite ReAct metadata. @@ -395,8 +424,8 @@ def _handle_react_chain_start(self, run: Run) -> None: else: self._start_agent(run) - def _resolve_langgraph_agent_name(self, run: Run) -> str: - """Pick a meaningful agent name for a LangGraph ReAct agent. + def _resolve_agent_name(self, run: Run) -> str: + """Pick a meaningful name for a LangGraph-based agent. When the inner graph uses the default name ``"LangGraph"`` and is invoked as a node inside an outer graph, the parent node's name @@ -404,6 +433,34 @@ def _resolve_langgraph_agent_name(self, run: Run) -> str: over the generic default. """ name = run.name or "" + agent_semantics = get_agent_semantics(run) + if ( + agent_semantics is not None + and agent_semantics[0] == DEERFLOW_FRAMEWORK + ): + if name not in {"", "LangGraph", "default"}: + return "lead-agent" if name == "lead_agent" else name + + metadata = getattr(run, "metadata", None) or {} + for key in ("_loongsuite_agent_name", "agent_name"): + value = metadata.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + + tags = getattr(run, "tags", None) or [] + for tag in tags: + if isinstance(tag, str) and tag.startswith("subagent:"): + return tag + + trace_name = metadata.get("langfuse_trace_name") + if isinstance(trace_name, str) and trace_name.strip(): + return trace_name.strip() + + baggage_name = baggage.get_baggage("gen_ai.agent.name") + if isinstance(baggage_name, str) and baggage_name.strip(): + return baggage_name.strip() + return "lead-agent" + if not _has_langgraph_react_metadata(run) or name != "LangGraph": return name @@ -446,13 +503,16 @@ def _start_agent(self, run: Run) -> None: if converted: input_messages.append(converted) - agent_name = self._resolve_langgraph_agent_name(run) + agent_name = self._resolve_agent_name(run) invocation = InvokeAgentInvocation( provider="langchain", agent_name=agent_name, input_messages=input_messages, ) + agent_semantics = get_agent_semantics(run) + if agent_semantics is not None: + invocation.attributes["gen_ai.framework"] = agent_semantics[0] self._handler.start_invoke_agent(invocation, context=parent_ctx) rd = _RunData( run_kind="agent", @@ -461,6 +521,8 @@ def _start_agent(self, run: Run) -> None: invocation=invocation, is_langgraph_react=_has_langgraph_react_metadata(run), is_deepagents_react=_has_deepagents_metadata(run), + agent_framework=(agent_semantics[0] if agent_semantics else None), + agent_step_node=(agent_semantics[1] if agent_semantics else None), ) with self._lock: self._runs[run.id] = rd @@ -488,10 +550,12 @@ def _start_chain(self, run: Run) -> None: ctx = set_span_in_context(span, current_context) token = otel_context.attach(ctx) - # Propagate framework-specific ReAct context from parent so that + # Propagate framework-specific agent context from parent so that # grandchildren of the graph are also recognised as internal. inside_lg = False inside_deepagents = False + inside_agent_framework = None + inside_agent_step_node = None deepagents_skills_by_path: dict[str, dict[str, Any]] = {} parent_id = getattr(run, "parent_run_id", None) if parent_id: @@ -502,6 +566,12 @@ def _start_chain(self, run: Run) -> None: inside_deepagents = ( p.is_deepagents_react or p.inside_deepagents_react ) + inside_agent_framework = ( + p.agent_framework or p.inside_agent_framework + ) + inside_agent_step_node = ( + p.agent_step_node or p.inside_agent_step_node + ) deepagents_skills_by_path = dict(p.deepagents_skills_by_path) rd = _RunData( @@ -511,6 +581,8 @@ def _start_chain(self, run: Run) -> None: context_token=token, inside_langgraph_react=inside_lg, inside_deepagents_react=inside_deepagents, + inside_agent_framework=inside_agent_framework, + inside_agent_step_node=inside_agent_step_node, deepagents_skills_by_path=deepagents_skills_by_path, ) with self._lock: @@ -731,14 +803,11 @@ def _on_retriever_error(self, run: Run) -> None: logger.debug("Failed to fail Retriever span", exc_info=True) # ------------------------------------------------------------------ - # LangGraph ReAct Step — callback-based detection + # Agent STEP — callback-based detection # ------------------------------------------------------------------ - def _maybe_enter_langgraph_react_step(self, run: Run) -> None: - """If *run* is a child node of a LangGraph ReAct agent whose name - equals ``LANGGRAPH_REACT_STEP_NODE`` (``"agent"``), trigger a ReAct - step transition: end the previous step (with ``"tool_calls"``) and - start a new one. + def _maybe_enter_semantic_agent_step(self, run: Run) -> None: + """Start a STEP for the opt-in graph's direct decision node. Must be called **before** ``_start_chain`` so that the chain span is parented under the step span. @@ -747,6 +816,27 @@ def _maybe_enter_langgraph_react_step(self, run: Run) -> None: if not parent_id: return + with self._lock: + parent_rd = self._runs.get(parent_id) + if parent_rd is None or parent_rd.agent_framework is None: + return + + chain_name = getattr(run, "name", "") or "" + if chain_name != parent_rd.agent_step_node: + return + + # End previous step (it had tool_calls since another round started) + if parent_rd.active_step is not None: + self._exit_react_step(parent_id, "tool_calls") + + self._enter_react_step(parent_id) + + def _maybe_enter_langgraph_react_step(self, run: Run) -> None: + """Start a ReAct STEP for a direct LangGraph ``agent`` node.""" + parent_id = getattr(run, "parent_run_id", None) + if not parent_id: + return + with self._lock: parent_rd = self._runs.get(parent_id) if parent_rd is None or not parent_rd.is_langgraph_react: @@ -756,19 +846,13 @@ def _maybe_enter_langgraph_react_step(self, run: Run) -> None: if chain_name != LANGGRAPH_REACT_STEP_NODE: return - # End previous step (it had tool_calls since another round started) if parent_rd.active_step is not None: self._exit_react_step(parent_id, "tool_calls") self._enter_react_step(parent_id) def _maybe_enter_deepagents_react_step(self, run: Run) -> None: - """Start a ReAct STEP for DeepAgents model decision nodes. - - DeepAgents is built through ``langchain.agents.create_agent`` and its - model decision node is named ``"model"``. Treat each direct ``model`` - child of the marked DeepAgents root graph as one ReAct round. - """ + """Start a ReAct STEP for a direct DeepAgents ``model`` node.""" parent_id = getattr(run, "parent_run_id", None) if not parent_id: return diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_semantics.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_semantics.py new file mode 100644 index 000000000..acee5ced3 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_semantics.py @@ -0,0 +1,54 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from types import SimpleNamespace + +import pytest + +from opentelemetry.instrumentation.langchain.internal._agent_semantics import ( + AGENT_FRAMEWORK_METADATA_KEY, + AGENT_STEP_NODE_METADATA_KEY, + get_agent_semantics, +) + + +def test_agent_semantics_requires_both_scalar_fields(): + run = SimpleNamespace( + metadata={ + AGENT_FRAMEWORK_METADATA_KEY: "deerflow", + AGENT_STEP_NODE_METADATA_KEY: "model", + } + ) + + assert get_agent_semantics(run) == ("deerflow", "model") + + +@pytest.mark.parametrize( + "metadata", + [ + {}, + {AGENT_FRAMEWORK_METADATA_KEY: "deerflow"}, + {AGENT_STEP_NODE_METADATA_KEY: "model"}, + { + AGENT_FRAMEWORK_METADATA_KEY: ["deerflow"], + AGENT_STEP_NODE_METADATA_KEY: "model", + }, + { + AGENT_FRAMEWORK_METADATA_KEY: "deerflow", + AGENT_STEP_NODE_METADATA_KEY: "", + }, + ], +) +def test_agent_semantics_rejects_incomplete_or_non_string_metadata(metadata): + assert get_agent_semantics(SimpleNamespace(metadata=metadata)) is None diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_spans.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_spans.py index 71d169fbb..0411231fb 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_spans.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_spans.py @@ -16,6 +16,14 @@ from uuid import uuid4 +import pytest + +from opentelemetry import baggage, context +from opentelemetry.instrumentation.langchain.internal._agent_semantics import ( + AGENT_FRAMEWORK_METADATA_KEY, + AGENT_STEP_NODE_METADATA_KEY, + DEERFLOW_FRAMEWORK, +) from opentelemetry.instrumentation.langchain.internal._tracer import ( LoongsuiteTracer, _extract_langgraph_input_message, @@ -162,3 +170,95 @@ def test_agent_context_colors_child_llm_and_tool_spans( assert llm_span.attributes[GenAI.GEN_AI_AGENT_NAME] == "AgentExecutor" assert tool_span.attributes[GenAI.GEN_AI_AGENT_NAME] == "AgentExecutor" + + +def test_deerflow_agent_semantics_sets_framework_attribute( + tracer_provider, span_exporter +): + handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) + tracer = LoongsuiteTracer( + handler=handler, + tracer_provider=tracer_provider, + ) + agent_run = _FakeRun("lead-agent", inputs={"input": "research"}) + agent_run.metadata = { + AGENT_FRAMEWORK_METADATA_KEY: DEERFLOW_FRAMEWORK, + AGENT_STEP_NODE_METADATA_KEY: "model", + } + + tracer._start_agent(agent_run) + agent_run.outputs = {"output": "done"} + tracer._on_chain_end(agent_run) + + agent_span = next( + span + for span in span_exporter.get_finished_spans() + if span.attributes.get("gen_ai.span.kind") == "AGENT" + ) + assert agent_span.attributes["gen_ai.framework"] == "deerflow" + + +@pytest.mark.parametrize( + ("name", "metadata", "tags", "expected"), + [ + ("research-agent", {}, [], "research-agent"), + ( + "LangGraph", + {"agent_name": "configured-agent"}, + [], + "configured-agent", + ), + ( + "LangGraph", + {"langfuse_trace_name": "metadata-agent"}, + ["subagent:researcher"], + "subagent:researcher", + ), + ( + "LangGraph", + {"langfuse_trace_name": "metadata-agent"}, + [], + "metadata-agent", + ), + ("default", {}, [], "lead-agent"), + ], +) +def test_deerflow_agent_name_resolution( + name, + metadata, + tags, + expected, + tracer_provider, +): + tracer = LoongsuiteTracer( + handler=ExtendedTelemetryHandler(tracer_provider=tracer_provider), + tracer_provider=tracer_provider, + ) + run = _FakeRun(name) + run.metadata = { + AGENT_FRAMEWORK_METADATA_KEY: DEERFLOW_FRAMEWORK, + AGENT_STEP_NODE_METADATA_KEY: "model", + **metadata, + } + run.tags = tags + + assert tracer._resolve_agent_name(run) == expected + + +def test_deerflow_agent_name_falls_back_to_entry_baggage(tracer_provider): + tracer = LoongsuiteTracer( + handler=ExtendedTelemetryHandler(tracer_provider=tracer_provider), + tracer_provider=tracer_provider, + ) + run = _FakeRun("LangGraph") + run.metadata = { + AGENT_FRAMEWORK_METADATA_KEY: DEERFLOW_FRAMEWORK, + AGENT_STEP_NODE_METADATA_KEY: "model", + } + run.tags = [] + ctx = baggage.set_baggage("gen_ai.agent.name", "embedded-agent") + token = context.attach(ctx) + try: + assert tracer._resolve_agent_name(run) == "embedded-agent" + finally: + context.detach(token) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/CHANGELOG.md index 664eaa2de..63135c869 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Propagate opt-in framework and decision-node metadata for LangGraph-based + agent harnesses through `Pregel.stream` and `Pregel.astream` without + changing the existing prebuilt ReAct marker behavior. + ## Version 0.7.0 (2026-07-03) There are no changelog entries for this release. diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/src/opentelemetry/instrumentation/langgraph/internal/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/src/opentelemetry/instrumentation/langgraph/internal/patch.py index ac057bcdf..9d39bde4b 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/src/opentelemetry/instrumentation/langgraph/internal/patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/src/opentelemetry/instrumentation/langgraph/internal/patch.py @@ -21,14 +21,12 @@ def wrapper(wrapped, instance, args, kwargs) -> ... Three patch targets: 1. ``create_react_agent`` — sets ``_loongsuite_react_agent = True`` on the - compiled ``CompiledStateGraph`` so that downstream instrumentation can - recognise it as a ReAct agent. + compiled ``CompiledStateGraph`` so downstream instrumentation recognises it. 2. ``Pregel.stream`` / ``Pregel.astream`` — injects - ``metadata["_loongsuite_react_agent"] = True`` into the ``RunnableConfig`` - when the graph is a marked ReAct agent. This metadata flows through - LangChain's callback system to ``Run.metadata``, where the - ``LoongsuiteTracer`` reads it to create Agent and ReAct Step spans. + either the existing ReAct marker or an opt-in harness's framework and + decision-node semantics into a copy of ``RunnableConfig``. The metadata + then flows through LangChain callbacks to ``Run.metadata``. """ from __future__ import annotations @@ -38,6 +36,8 @@ def wrapper(wrapped, instance, args, kwargs) -> ... logger = logging.getLogger(__name__) +AGENT_FRAMEWORK_METADATA_KEY = "_loongsuite_agent_framework" +AGENT_STEP_NODE_METADATA_KEY = "_loongsuite_agent_step_node" REACT_AGENT_METADATA_KEY = "_loongsuite_react_agent" @@ -70,10 +70,10 @@ def _create_react_agent_wrapper( # --------------------------------------------------------------------------- -def _inject_react_metadata(config: Any) -> Any: - """Return a *new* config dict with ``_loongsuite_react_agent: True`` - in its ``metadata``. - """ +def _copy_config_and_metadata( + config: Any, +) -> tuple[dict[str, Any], dict[str, Any]]: + """Return copy-on-write config and metadata dictionaries.""" # Inline import: langchain_core is a transitive dependency of langgraph; # importing here avoids module-level coupling. from langchain_core.runnables.config import ( # noqa: PLC0415 @@ -82,15 +82,44 @@ def _inject_react_metadata(config: Any) -> Any: config = ensure_config(config) config = {**config} - metadata = dict(config.get("metadata") or {}) + return config, dict(config.get("metadata") or {}) + + +def _inject_react_metadata(config: Any) -> Any: + """Return a new config carrying the existing prebuilt ReAct marker.""" + config, metadata = _copy_config_and_metadata(config) metadata.setdefault(REACT_AGENT_METADATA_KEY, True) config["metadata"] = metadata return config +def _inject_agent_semantics(config: Any, semantics: tuple[str, str]) -> Any: + """Return a new config carrying an opt-in harness's scalar semantics.""" + config, metadata = _copy_config_and_metadata(config) + metadata[AGENT_FRAMEWORK_METADATA_KEY] = semantics[0] + metadata[AGENT_STEP_NODE_METADATA_KEY] = semantics[1] + config["metadata"] = metadata + return config + + +def _get_graph_agent_semantics(graph: Any) -> tuple[str, str] | None: + """Read validated opt-in agent semantics from graph attributes.""" + + framework = getattr(graph, AGENT_FRAMEWORK_METADATA_KEY, None) + step_node = getattr(graph, AGENT_STEP_NODE_METADATA_KEY, None) + if not isinstance(framework, str) or not framework.strip(): + return None + if not isinstance(step_node, str) or not step_node.strip(): + return None + return framework.strip(), step_node.strip() + + def _stream_wrapper(wrapped: Any, instance: Any, args: Any, kwargs: Any): # type: ignore[return] """``wrapt`` wrapper for ``Pregel.stream``.""" - if getattr(instance, REACT_AGENT_METADATA_KEY, False): + semantics = _get_graph_agent_semantics(instance) + if semantics is not None: + args, kwargs = _rewrite_config(args, kwargs, semantics) + elif getattr(instance, REACT_AGENT_METADATA_KEY, False): args, kwargs = _rewrite_config(args, kwargs) yield from wrapped(*args, **kwargs) @@ -99,20 +128,33 @@ async def _astream_wrapper( wrapped: Any, instance: Any, args: Any, kwargs: Any ): # type: ignore[return] """``wrapt`` wrapper for ``Pregel.astream``.""" - if getattr(instance, REACT_AGENT_METADATA_KEY, False): + semantics = _get_graph_agent_semantics(instance) + if semantics is not None: + args, kwargs = _rewrite_config(args, kwargs, semantics) + elif getattr(instance, REACT_AGENT_METADATA_KEY, False): args, kwargs = _rewrite_config(args, kwargs) async for chunk in wrapped(*args, **kwargs): yield chunk def _rewrite_config( - args: tuple[Any, ...], kwargs: dict[str, Any] + args: tuple[Any, ...], + kwargs: dict[str, Any], + semantics: tuple[str, str] | None = None, ) -> tuple[tuple[Any, ...], dict[str, Any]]: """Extract ``config`` from *args*/*kwargs*, inject metadata, put it back.""" if len(args) > 1: - config = _inject_react_metadata(args[1]) + config = ( + _inject_agent_semantics(args[1], semantics) + if semantics is not None + else _inject_react_metadata(args[1]) + ) args = (args[0], config) + args[2:] else: - config = _inject_react_metadata(kwargs.get("config")) + config = ( + _inject_agent_semantics(kwargs.get("config"), semantics) + if semantics is not None + else _inject_react_metadata(kwargs.get("config")) + ) kwargs = {**kwargs, "config": config} return args, kwargs diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_patch.py index 1aa94abdc..6e633ac79 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_patch.py @@ -25,6 +25,14 @@ from langchain_core.tools import tool from opentelemetry.instrumentation.langgraph import LangGraphInstrumentor +from opentelemetry.instrumentation.langgraph.internal.patch import ( + AGENT_FRAMEWORK_METADATA_KEY, + AGENT_STEP_NODE_METADATA_KEY, + REACT_AGENT_METADATA_KEY, + _get_graph_agent_semantics, + _inject_agent_semantics, + _inject_react_metadata, +) # --------------------------------------------------------------------------- # Helpers @@ -156,6 +164,54 @@ class _State(dict): assert not getattr(plain_graph, "_loongsuite_react_agent", False) + def test_metadata_injection_is_copy_on_write(self): + callback = object() + original = { + "callbacks": [callback], + "metadata": {"customer": "kept"}, + } + + rewritten = _inject_react_metadata(original) + + assert rewritten is not original + assert rewritten["metadata"] is not original["metadata"] + assert original == { + "callbacks": [callback], + "metadata": {"customer": "kept"}, + } + assert rewritten["callbacks"] == [callback] + assert rewritten["metadata"]["customer"] == "kept" + assert rewritten["metadata"][REACT_AGENT_METADATA_KEY] is True + + def test_opt_in_semantics_do_not_require_legacy_marker(self): + graph = type("Graph", (), {})() + setattr(graph, AGENT_FRAMEWORK_METADATA_KEY, "deerflow") + setattr(graph, AGENT_STEP_NODE_METADATA_KEY, "model") + + assert _get_graph_agent_semantics(graph) == ("deerflow", "model") + + def test_agent_semantics_injection_is_copy_on_write(self): + callback = object() + original = { + "callbacks": [callback], + "metadata": {"customer": "kept"}, + } + + rewritten = _inject_agent_semantics( + original, + ("deerflow", "model"), + ) + + assert rewritten is not original + assert rewritten["metadata"] is not original["metadata"] + assert original["metadata"] == {"customer": "kept"} + assert rewritten["callbacks"] == [callback] + assert rewritten["metadata"]["customer"] == "kept" + assert ( + rewritten["metadata"][AGENT_FRAMEWORK_METADATA_KEY] == "deerflow" + ) + assert rewritten["metadata"][AGENT_STEP_NODE_METADATA_KEY] == "model" + def test_uninstrument_restores_stream(self): """After uninstrument, Pregel.stream is no longer a wrapt wrapper.""" from langgraph.pregel import Pregel # noqa: PLC0415 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_react_step_spans.py b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_react_step_spans.py index 3bc398b84..06d5e686e 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_react_step_spans.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_react_step_spans.py @@ -21,7 +21,7 @@ from __future__ import annotations -from typing import Any, List, Optional, Sequence +from typing import Any, List, Optional, Sequence, TypedDict from langchain_core.callbacks import CallbackManagerForLLMRun from langchain_core.language_models.chat_models import BaseChatModel @@ -102,6 +102,31 @@ def _build_react_agent(responses: list[AIMessage], **extra: Any): return create_react_agent(llm, [_dummy_tool], **extra) +def _build_opt_in_harness(): + """Build a plain StateGraph marked by a framework adapter.""" + from langgraph.graph import StateGraph # noqa: PLC0415 + + class _State(TypedDict): + result: str + + llm = _FakeChatModelWithTools( + responses=[AIMessage(content="Harness answer.")] + ) + + def decision_node(state: _State) -> _State: + message = llm.invoke("hello") + return {**state, "result": str(message.content)} + + builder = StateGraph(_State) + builder.add_node("decision", decision_node) + builder.set_entry_point("decision") + builder.set_finish_point("decision") + graph = builder.compile(name="example-harness") + graph._loongsuite_agent_framework = "example-harness" + graph._loongsuite_agent_step_node = "decision" + return graph + + # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- @@ -167,6 +192,40 @@ def test_span_hierarchy(self, instrument, span_exporter): assert step.parent.span_id == agent.context.span_id +def test_opt_in_state_graph_uses_declared_decision_node( + instrument, span_exporter +): + """A LangGraph-based harness can opt in without changing prebuilt rules.""" + graph = _build_opt_in_harness() + + result = graph.invoke({"result": ""}) + + assert result["result"] == "Harness answer." + spans = span_exporter.get_finished_spans() + agent_spans = [ + span + for span in spans + if span.attributes.get("gen_ai.span.kind") == "AGENT" + ] + step_spans = [ + span + for span in spans + if span.attributes.get("gen_ai.span.kind") == "STEP" + ] + llm_spans = [ + span + for span in spans + if span.attributes.get("gen_ai.span.kind") == "LLM" + ] + + assert len(agent_spans) == 1 + assert agent_spans[0].attributes["gen_ai.framework"] == "example-harness" + assert len(step_spans) == 1 + assert step_spans[0].parent.span_id == agent_spans[0].context.span_id + assert len(llm_spans) == 1 + assert llm_spans[0].context.trace_id == agent_spans[0].context.trace_id + + class TestLangGraphReActMultiRound: """Multi-round agent: LLM uses a tool in round 1, answers in round 2.""" diff --git a/tox-loongsuite.ini b/tox-loongsuite.ini index 6482b0196..df6c6fb70 100644 --- a/tox-loongsuite.ini +++ b/tox-loongsuite.ini @@ -53,6 +53,11 @@ envlist = py3{11,12,13}-test-loongsuite-instrumentation-deepagents-latest lint-loongsuite-instrumentation-deepagents + ; loongsuite-instrumentation-deerflow + py312-test-loongsuite-instrumentation-deerflow.stable + py3{12,13}-test-loongsuite-instrumentation-deerflow.current + lint-loongsuite-instrumentation-deerflow + ; loongsuite-instrumentation-microsoft-agent-framework py3{10,11,12,13}-test-loongsuite-instrumentation-microsoft-agent-framework lint-loongsuite-instrumentation-microsoft-agent-framework @@ -200,6 +205,24 @@ deps = lint-loongsuite-instrumentation-deepagents: {[testenv]test_deps} lint-loongsuite-instrumentation-deepagents: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/requirements.latest.txt + ; DeerFlow requires Langfuse, whose OTLP exporter must use the same + ; development OTel stack as the rest of this test environment. + loongsuite-instrumentation-deerflow.stable: opentelemetry-proto@{env:CORE_REPO}\#egg=opentelemetry-proto&subdirectory=opentelemetry-proto + loongsuite-instrumentation-deerflow.stable: opentelemetry-exporter-otlp-proto-common@{env:CORE_REPO}\#egg=opentelemetry-exporter-otlp-proto-common&subdirectory=exporter/opentelemetry-exporter-otlp-proto-common + loongsuite-instrumentation-deerflow.stable: opentelemetry-exporter-otlp-proto-http@{env:CORE_REPO}\#egg=opentelemetry-exporter-otlp-proto-http&subdirectory=exporter/opentelemetry-exporter-otlp-proto-http + loongsuite-instrumentation-deerflow.stable: {[testenv]test_deps} + loongsuite-instrumentation-deerflow.stable: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.oldest.txt + loongsuite-instrumentation-deerflow.current: opentelemetry-proto@{env:CORE_REPO}\#egg=opentelemetry-proto&subdirectory=opentelemetry-proto + loongsuite-instrumentation-deerflow.current: opentelemetry-exporter-otlp-proto-common@{env:CORE_REPO}\#egg=opentelemetry-exporter-otlp-proto-common&subdirectory=exporter/opentelemetry-exporter-otlp-proto-common + loongsuite-instrumentation-deerflow.current: opentelemetry-exporter-otlp-proto-http@{env:CORE_REPO}\#egg=opentelemetry-exporter-otlp-proto-http&subdirectory=exporter/opentelemetry-exporter-otlp-proto-http + loongsuite-instrumentation-deerflow.current: {[testenv]test_deps} + loongsuite-instrumentation-deerflow.current: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.latest.txt + lint-loongsuite-instrumentation-deerflow: opentelemetry-proto@{env:CORE_REPO}\#egg=opentelemetry-proto&subdirectory=opentelemetry-proto + lint-loongsuite-instrumentation-deerflow: opentelemetry-exporter-otlp-proto-common@{env:CORE_REPO}\#egg=opentelemetry-exporter-otlp-proto-common&subdirectory=exporter/opentelemetry-exporter-otlp-proto-common + lint-loongsuite-instrumentation-deerflow: opentelemetry-exporter-otlp-proto-http@{env:CORE_REPO}\#egg=opentelemetry-exporter-otlp-proto-http&subdirectory=exporter/opentelemetry-exporter-otlp-proto-http + lint-loongsuite-instrumentation-deerflow: {[testenv]test_deps} + lint-loongsuite-instrumentation-deerflow: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.oldest.txt + microsoft-agent-framework: {[testenv]test_deps} microsoft-agent-framework: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests/requirements.latest.txt lint-loongsuite-instrumentation-microsoft-agent-framework: {[testenv]test_deps} @@ -323,6 +346,10 @@ commands = test-loongsuite-instrumentation-deepagents: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests {posargs} lint-loongsuite-instrumentation-deepagents: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deepagents + test-loongsuite-instrumentation-deerflow.stable: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests {posargs} + test-loongsuite-instrumentation-deerflow.current: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests {posargs} + lint-loongsuite-instrumentation-deerflow: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deerflow + test-loongsuite-instrumentation-microsoft-agent-framework: pytest {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework/tests {posargs} lint-loongsuite-instrumentation-microsoft-agent-framework: python -m ruff check {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-microsoft-agent-framework diff --git a/util/opentelemetry-util-genai/CHANGELOG-loongsuite.md b/util/opentelemetry-util-genai/CHANGELOG-loongsuite.md index 3812b57b4..8b954fef4 100644 --- a/util/opentelemetry-util-genai/CHANGELOG-loongsuite.md +++ b/util/opentelemetry-util-genai/CHANGELOG-loongsuite.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +- Preserve an internal ENTRY-active context marker across nested spans so + framework adapters can honor an existing host application entry without + creating duplicate ENTRY spans. + ## Version 0.7.0 (2026-07-03) ### Added diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/extended_handler.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/extended_handler.py index 3e6cfee7e..8678e444d 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/extended_handler.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/extended_handler.py @@ -128,6 +128,16 @@ from opentelemetry.util.genai.span_utils import _apply_error_attributes from opentelemetry.util.genai.types import Error, LLMInvocation +_ENTRY_ACTIVE_CONTEXT_KEY = "loongsuite.gen_ai.entry.active" + + +def is_entry_context_active(context: Context | None = None) -> bool: + """Return whether the current context is inside a LoongSuite ENTRY.""" + + return bool( + otel_context.get_value(_ENTRY_ACTIVE_CONTEXT_KEY, context=context) + ) + class ExtendedTelemetryHandler(MultimodalProcessingMixin, TelemetryHandler): # pylint: disable=too-many-public-methods """ @@ -815,6 +825,7 @@ def start_entry( current_context = _current_context(context) _inject_agent_name_from_baggage(invocation, current_context) ctx = set_span_in_context(span, current_context) + ctx = otel_context.set_value(_ENTRY_ACTIVE_CONTEXT_KEY, True, ctx) if invocation.session_id is not None: ctx = baggage.set_baggage( _GEN_AI_SESSION_ID, invocation.session_id, ctx diff --git a/util/opentelemetry-util-genai/tests/test_extended_handler.py b/util/opentelemetry-util-genai/tests/test_extended_handler.py index e279d9f43..9fe8b7089 100644 --- a/util/opentelemetry-util-genai/tests/test_extended_handler.py +++ b/util/opentelemetry-util-genai/tests/test_extended_handler.py @@ -61,6 +61,7 @@ from opentelemetry.util.genai.extended_handler import ( ExtendedTelemetryHandler, get_extended_telemetry_handler, + is_entry_context_active, ) from opentelemetry.util.genai.extended_semconv.gen_ai_extended_attributes import ( GEN_AI_EMBEDDINGS_DIMENSION_COUNT, @@ -1465,6 +1466,17 @@ def test_entry_propagates_baggage_for_child_spans(self): self.assertNotIn("gen_ai.session.id", restored_baggage) self.assertNotIn("gen_ai.user.id", restored_baggage) + def test_entry_context_marker_survives_nested_child_span(self): + """ENTRY ancestry remains detectable when a child span is current.""" + self.assertFalse(is_entry_context_active()) + + with self.telemetry_handler.entry(): + self.assertTrue(is_entry_context_active()) + with self.telemetry_handler.embedding(): + self.assertTrue(is_entry_context_active()) + + self.assertFalse(is_entry_context_active()) + def test_entry_baggage_overwrites_existing(self): """If baggage already contains session_id/user_id, entry overwrites them.""" ctx = baggage_api.set_baggage("gen_ai.session.id", "old_session")