From 6a14f7251f157bb20dc644d8602e915ebf6f93d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= <53717475+sipercai@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:47:13 +0800 Subject: [PATCH 1/6] feat: add DeerFlow 2.x observability --- .github/workflows/loongsuite_lint_0.yml | 2 +- .github/workflows/loongsuite_test_0.yml | 2 +- instrumentation-loongsuite/README.md | 1 + .../CHANGELOG.md | 6 + .../deepagents/internal/patch.py | 13 +- .../tests/test_deepagents_spans.py | 149 ++++ .../tests/test_instrumentor.py | 20 + .../CHANGELOG.md | 21 + .../README.md | 64 ++ .../pyproject.toml | 52 ++ .../instrumentation/deerflow/__init__.py | 188 +++++ .../deerflow/internal/__init__.py | 13 + .../deerflow/internal/constants.py | 48 ++ .../deerflow/internal/patch.py | 748 +++++++++++++++++ .../deerflow/internal/utils.py | 258 ++++++ .../instrumentation/deerflow/package.py | 20 + .../instrumentation/deerflow/version.py | 15 + .../tests/conftest.py | 69 ++ .../tests/requirements.latest.txt | 10 + .../tests/requirements.oldest.txt | 10 + .../tests/test_entry_wrappers.py | 749 ++++++++++++++++++ .../tests/test_instrumentor.py | 199 +++++ .../tests/test_runtime_graph.py | 485 ++++++++++++ .../tests/test_subagents.py | 373 +++++++++ .../CHANGELOG.md | 6 + .../instrumentation/langchain/__init__.py | 16 +- .../langchain/internal/_agent_flavor.py | 60 ++ .../langchain/internal/_tracer.py | 130 ++- .../langchain/internal/_utils.py | 14 +- .../tests/test_agent_flavor.py | 87 ++ .../tests/test_agent_spans.py | 92 +++ .../CHANGELOG.md | 6 + .../langgraph/internal/patch.py | 63 +- .../tests/test_patch.py | 45 ++ .../loongsuite_instrumentation_deerflow.py | 28 + tox-loongsuite.ini | 16 + .../CHANGELOG-loongsuite.md | 6 + .../util/genai/extended_handler.py | 11 + .../tests/test_extended_handler.py | 12 + 39 files changed, 3996 insertions(+), 111 deletions(-) create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/pyproject.toml create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/__init__.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/__init__.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/constants.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/patch.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/utils.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/package.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/version.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/conftest.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.latest.txt create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.oldest.txt create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_subagents.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_flavor.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_flavor.py create mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deerflow.py 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..39f58cc71 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 | 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 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/CHANGELOG.md index 0c4bf074b..eb3dbf67f 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Mark DeepAgents graphs with the explicit `deepagents` agent flavor so their + `model` nodes remain the sole ReAct `STEP` boundary when multiple compatible + instrumentors are enabled. + ## Version 0.7.0 (2026-07-03) ### Added diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/patch.py index ee78ba8e4..c9a06bcc2 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/patch.py @@ -16,10 +16,9 @@ DeepAgents builds on ``langchain.agents.create_agent`` and returns ``create_agent(...).with_config(...)``. That final ``with_config`` call creates -a new graph object and drops the marker that LangChain instrumentation places -on the original graph. This module restores the marker on the returned graph -and injects the marker into call-time config, mirroring LangGraph's lightweight -metadata handoff. +a new graph object and drops the flavor that LangChain instrumentation places +on the original graph. This module marks the returned graph as ``deepagents`` +and injects that flavor into call-time config. """ from __future__ import annotations @@ -40,6 +39,8 @@ CREATE_DEEP_AGENT_MODULE = "deepagents.graph" CREATE_DEEP_AGENT_NAME = "create_deep_agent" +AGENT_FLAVOR_METADATA_KEY = "_loongsuite_agent_flavor" +DEEPAGENTS_AGENT_FLAVOR = "deepagents" REACT_AGENT_METADATA_KEY = "_loongsuite_react_agent" DEEPAGENTS_METADATA_KEY = "_loongsuite_deepagents_agent" GRAPH_METHODS_WRAPPED_ATTR = "_loongsuite_deepagents_methods_wrapped" @@ -181,6 +182,7 @@ def _restore_top_level_create_deep_agent() -> None: def _mark_graph(graph: Any) -> None: with suppress(Exception): + setattr(graph, AGENT_FLAVOR_METADATA_KEY, DEEPAGENTS_AGENT_FLAVOR) setattr(graph, REACT_AGENT_METADATA_KEY, True) setattr(graph, DEEPAGENTS_METADATA_KEY, True) @@ -241,6 +243,7 @@ def _restore_graph_methods(graph: Any) -> None: for attr_name in ( GRAPH_ORIGINAL_METHODS_ATTR, GRAPH_METHODS_WRAPPED_ATTR, + AGENT_FLAVOR_METADATA_KEY, REACT_AGENT_METADATA_KEY, DEEPAGENTS_METADATA_KEY, ): @@ -313,6 +316,8 @@ def _inject_react_metadata(config: Any) -> Any: config = ensure_config(config) config = {**config} metadata = dict(config.get("metadata") or {}) + metadata[AGENT_FLAVOR_METADATA_KEY] = DEEPAGENTS_AGENT_FLAVOR + metadata.setdefault(REACT_AGENT_METADATA_KEY, True) metadata.setdefault(DEEPAGENTS_METADATA_KEY, True) config["metadata"] = metadata return config diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_deepagents_spans.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_deepagents_spans.py index e0b45cd51..8174a2b7b 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_deepagents_spans.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_deepagents_spans.py @@ -28,6 +28,12 @@ from langchain_core.outputs import ChatGeneration, ChatResult from langchain_core.tools import tool +from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( + AGENT_FLAVOR_METADATA_KEY, + DEEPAGENTS_AGENT_FLAVOR, + LANGCHAIN_CREATE_AGENT_FLAVOR, +) + class _SequenceToolCallingModel(BaseChatModel): def __init__(self, responses: list[AIMessage]): @@ -156,6 +162,148 @@ def _assert_kind_under_step( return matches +def test_langchain_create_agent_model_node_creates_single_step( + instrument, span_exporter +): + """LangChain 1.x ``create_agent`` uses ``model``, not ``agent``, nodes.""" + from langchain.agents import create_agent # noqa: PLC0415 + + agent = create_agent( + model=_SequenceToolCallingModel( + [AIMessage(content="Plain LangChain final answer.")] + ), + tools=[lookup_city], + name="plain_langchain_agent", + ) + + assert ( + getattr(agent, AGENT_FLAVOR_METADATA_KEY) + == LANGCHAIN_CREATE_AGENT_FLAVOR + ) + assert not hasattr(agent, "_loongsuite_deepagents_agent") + + result = agent.invoke({"messages": [{"role": "user", "content": "hello"}]}) + + assert result["messages"][-1].content == "Plain LangChain final answer." + spans = span_exporter.get_finished_spans() + agent_span = _assert_single_agent(spans, "plain_langchain_agent") + step_spans = _spans_by_kind(spans, "STEP") + assert len(step_spans) == 1 + _assert_step_is_under_agent(agent_span, step_spans[0]) + + +def test_langchain_create_agent_tool_loop_creates_two_steps( + instrument, span_exporter +): + from langchain.agents import create_agent # noqa: PLC0415 + + agent = create_agent( + model=_SequenceToolCallingModel( + [ + AIMessage( + content="", + tool_calls=[ + { + "name": "lookup_city", + "args": {"query": "where"}, + "id": "plain_call_1", + } + ], + ), + AIMessage(content="Plain answer: Hangzhou."), + ] + ), + tools=[lookup_city], + name="plain_langchain_tool_agent", + ) + + result = agent.invoke({"messages": [{"role": "user", "content": "where"}]}) + + assert result["messages"][-1].content == "Plain answer: Hangzhou." + spans = span_exporter.get_finished_spans() + _assert_single_agent(spans, "plain_langchain_tool_agent") + step_spans = sorted( + _spans_by_kind(spans, "STEP"), + key=lambda span: span.attributes.get("gen_ai.react.round", 0), + ) + assert len(step_spans) == 2 + assert [ + span.attributes.get("gen_ai.react.finish_reason") + for span in step_spans + ] == ["tool_calls", "stop"] + + +def test_langchain_create_agent_stream_creates_single_step( + instrument, + span_exporter, +): + from langchain.agents import create_agent # noqa: PLC0415 + + agent = create_agent( + model=_SequenceToolCallingModel( + [AIMessage(content="Plain streamed answer.")] + ), + tools=[lookup_city], + name="plain_langchain_stream_agent", + ) + + chunks = list( + agent.stream({"messages": [{"role": "user", "content": "hello"}]}) + ) + + assert chunks + spans = span_exporter.get_finished_spans() + agent_span = _assert_single_agent(spans, "plain_langchain_stream_agent") + step_spans = _spans_by_kind(spans, "STEP") + assert len(step_spans) == 1 + _assert_step_is_under_agent(agent_span, step_spans[0]) + + +@pytest.mark.asyncio +async def test_langchain_create_agent_async_tool_loop_creates_two_steps( + instrument, + span_exporter, +): + from langchain.agents import create_agent # noqa: PLC0415 + + agent = create_agent( + model=_SequenceToolCallingModel( + [ + AIMessage( + content="", + tool_calls=[ + { + "name": "lookup_city", + "args": {"query": "where"}, + "id": "plain_async_call_1", + } + ], + ), + AIMessage(content="Async answer: Hangzhou."), + ] + ), + tools=[lookup_city], + name="plain_langchain_async_agent", + ) + + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": "where"}]} + ) + + assert result["messages"][-1].content == "Async answer: Hangzhou." + spans = span_exporter.get_finished_spans() + _assert_single_agent(spans, "plain_langchain_async_agent") + step_spans = sorted( + _spans_by_kind(spans, "STEP"), + key=lambda span: span.attributes.get("gen_ai.react.round", 0), + ) + assert len(step_spans) == 2 + assert [ + span.attributes.get("gen_ai.react.finish_reason") + for span in step_spans + ] == ["tool_calls", "stop"] + + def test_deepagents_root_span_is_agent_and_single_step( instrument, span_exporter ): @@ -166,6 +314,7 @@ def test_deepagents_root_span_is_agent_and_single_step( assert getattr(agent, "_loongsuite_react_agent") is True assert getattr(agent, "_loongsuite_deepagents_agent") is True + assert getattr(agent, AGENT_FLAVOR_METADATA_KEY) == DEEPAGENTS_AGENT_FLAVOR result = agent.invoke({"messages": [{"role": "user", "content": "hello"}]}) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_instrumentor.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_instrumentor.py index cf048f099..96ef8ce3a 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_instrumentor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_instrumentor.py @@ -18,6 +18,8 @@ from opentelemetry.instrumentation.deepagents import DeepAgentsInstrumentor from opentelemetry.instrumentation.deepagents.internal.patch import ( + AGENT_FLAVOR_METADATA_KEY, + DEEPAGENTS_AGENT_FLAVOR, DEEPAGENTS_METADATA_KEY, GRAPH_METHODS_WRAPPED_ATTR, GRAPH_ORIGINAL_METHODS_ATTR, @@ -99,10 +101,18 @@ def test_uninstrument_restores_wrapped_graph_methods(): _wrap_graph_methods(graph) assert getattr(graph, GRAPH_METHODS_WRAPPED_ATTR) is True + assert ( + getattr(graph, AGENT_FLAVOR_METADATA_KEY) + == DEEPAGENTS_AGENT_FLAVOR + ) assert getattr(graph, REACT_AGENT_METADATA_KEY) is True assert getattr(graph, DEEPAGENTS_METADATA_KEY) is True invoke_config = graph.invoke("hello") + assert ( + invoke_config["metadata"][AGENT_FLAVOR_METADATA_KEY] + == DEEPAGENTS_AGENT_FLAVOR + ) assert invoke_config["metadata"][DEEPAGENTS_METADATA_KEY] is True stream_iter = graph.stream("hello") @@ -114,19 +124,29 @@ def test_uninstrument_restores_wrapped_graph_methods(): child_graph = graph.with_config({"metadata": {"customer": "kept"}}) assert getattr(child_graph, GRAPH_METHODS_WRAPPED_ATTR) is True + assert ( + getattr(child_graph, AGENT_FLAVOR_METADATA_KEY) + == DEEPAGENTS_AGENT_FLAVOR + ) assert getattr(child_graph, DEEPAGENTS_METADATA_KEY) is True child_config = child_graph.invoke("hello") assert child_config["metadata"]["customer"] == "kept" + assert ( + child_config["metadata"][AGENT_FLAVOR_METADATA_KEY] + == DEEPAGENTS_AGENT_FLAVOR + ) assert child_config["metadata"][DEEPAGENTS_METADATA_KEY] is True uninstrument_create_deep_agent() assert not hasattr(graph, GRAPH_METHODS_WRAPPED_ATTR) assert not hasattr(graph, GRAPH_ORIGINAL_METHODS_ATTR) + assert not hasattr(graph, AGENT_FLAVOR_METADATA_KEY) assert not hasattr(graph, REACT_AGENT_METADATA_KEY) assert not hasattr(graph, DEEPAGENTS_METADATA_KEY) assert not hasattr(child_graph, GRAPH_METHODS_WRAPPED_ATTR) assert not hasattr(child_graph, GRAPH_ORIGINAL_METHODS_ATTR) + assert not hasattr(child_graph, AGENT_FLAVOR_METADATA_KEY) assert not hasattr(child_graph, REACT_AGENT_METADATA_KEY) assert not hasattr(child_graph, DEEPAGENTS_METADATA_KEY) assert graph.invoke("hello") is None diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md new file mode 100644 index 000000000..bc5f56d4e --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md @@ -0,0 +1,21 @@ +# 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 + +## 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..0bb81fb77 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md @@ -0,0 +1,64 @@ +# LoongSuite DeerFlow Instrumentation + +LoongSuite instrumentation for [DeerFlow](https://github.com/bytedance/deer-flow) 2.x. + +## Installation + +Install DeerFlow from its official Git repository, then install this package: + +```bash +pip install 'deerflow-harness @ git+https://github.com/bytedance/deer-flow.git@v2.0.0#subdirectory=backend/packages/harness' +pip install loongsuite-instrumentation-deerflow +``` + +The similarly named `deerflow-harness` package currently published on PyPI is +not the official DeerFlow 2.x distribution. This instrumentation deliberately +does not expose an `instruments` extra that would install it. + +## Usage + +```python +from opentelemetry.instrumentation.deerflow import DeerFlowInstrumentor + +DeerFlowInstrumentor().instrument() +``` + +The instrumentor composes the LoongSuite LangChain and LangGraph +instrumentations, identifies DeerFlow-created graphs, and creates an `ENTRY` +span around Gateway runs and embedded `DeerFlowClient.stream()` calls. Direct +SDK graph invocation starts at the graph's `AGENT` span and does not create a +synthetic application entry. + +The core execution tree is: + +```text +ENTRY deerflow +└── AGENT lead-agent + └── STEP + ├── LLM + └── TOOL task + └── AGENT subagent: +``` + +An existing host `ENTRY` takes precedence over the Gateway and embedded entry +points. The embedded iterator restores its call-time context around every +`next()` and `close()`, including cross-thread consumption, without leaking +that context back to the consumer. + +`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 expose their real `deerflow.run.id`. +Embedded runs omit that field because DeerFlow's public stream API does not +expose its internally generated run id. + +Message content is disabled by default and follows the existing experimental +GenAI content-capture switch. + +`task` remains a normal LangChain tool. This package does not patch DeerFlow's +sandbox, memory, skills, or context propagation internals. + +## Compatibility + +- DeerFlow `>=2,<3`, installed from the official source repository +- Python 3.12+ 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..7422cea31 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/__init__.py @@ -0,0 +1,188 @@ +# 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, + 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 _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 + + 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"), + ) + try: + self._deerflow_patched = instrument_deerflow(handler) + except BaseException: + uninstrument_deerflow() + for instrumentor in reversed(self._dependency_instrumentors): + instrumentor.uninstrument() + self._dependency_instrumentors = [] + raise + + def _uninstrument(self, **kwargs: Any) -> None: + del kwargs + if getattr(self, "_deerflow_patched", False): + uninstrument_deerflow() + self._deerflow_patched = False + + for instrumentor in reversed( + getattr(self, "_dependency_instrumentors", []) + ): + try: + instrumentor.uninstrument() + except Exception: # noqa: BLE001 + logger.debug( + "Failed to uninstrument DeerFlow dependency %s", + instrumentor.__class__.__name__, + exc_info=True, + ) + 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..d0e67cadf --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/constants.py @@ -0,0 +1,48 @@ +# 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_FLAVOR_ATTR = "_loongsuite_agent_flavor" +DEERFLOW_AGENT_FLAVOR = "deerflow" + +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..8e111d834 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/patch.py @@ -0,0 +1,748 @@ +# 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_FLAVOR_ATTR, + CLIENT_STREAM, + CREATE_AGENT_ALIASES, + DEERFLOW_AGENT_FLAVOR, + 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 the DeerFlow flavor and no legacy ReAct flags.""" + marker_attrs = (AGENT_FLAVOR_ATTR, *_LEGACY_GRAPH_ATTRS) + original_markers = { + name: getattr(graph, name, _MISSING) for name in marker_attrs + } + try: + setattr(graph, AGENT_FLAVOR_ATTR, DEERFLOW_AGENT_FLAVOR) + 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 (ImportError, ModuleNotFoundError): + pass + 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 (ImportError, ModuleNotFoundError): + 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 (ImportError, ModuleNotFoundError, RuntimeError, ValueError): + 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") + graph_input = _call_arg(args, kwargs, 5, "graph_input") + config = _call_arg(args, kwargs, 6, "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 + _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 == "success": + _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 + _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: + 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 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..8c8ce616f --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py @@ -0,0 +1,749 @@ +# 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 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 + assert list(result)[0].data["content"] == "world" + 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 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_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_fails_entry_and_closes_inner( + 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.ERROR + assert entry.attributes["error.type"] == "GeneratorExit" + 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.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"), + ("interrupted", "DeerFlowRunInterrupted"), + ], +) +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_cancellation_fails_entry_as_interrupted( + 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.ERROR + assert entry.attributes["error.type"] == "CancelledError" + 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..d655f70ef --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py @@ -0,0 +1,199 @@ +# 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 sys +from types import ModuleType + +import pytest + +from opentelemetry.instrumentation.deerflow import ( + DeerFlowInstrumentor, + _deerflow_runtime_supported, +) +from opentelemetry.instrumentation.deerflow.internal.constants import ( + AGENT_FLAVOR_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 test_has_no_pypi_target_library_dependency(): + assert _instruments == () + assert DeerFlowInstrumentor().instrumentation_dependencies() == () + + +@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_create_agent_alias_marks_only_new_flavor(): + graph = _Graph() + graph._loongsuite_react_agent = True + result = _create_agent_alias_wrapper( + lambda: graph, + None, + (), + {}, + ) + + assert result is graph + assert getattr(graph, AGENT_FLAVOR_ATTR) == "deerflow" + assert not hasattr(graph, "_loongsuite_react_agent") + assert not hasattr(graph, "_loongsuite_deepagents_agent") + + uninstrument_deerflow() + assert not hasattr(graph, AGENT_FLAVOR_ATTR) + assert graph._loongsuite_react_agent is True + + +def test_uninstrument_restores_previous_graph_flavor(): + graph = _Graph() + setattr(graph, AGENT_FLAVOR_ATTR, "langchain-create-agent") + + mark_deerflow_graph(graph) + assert getattr(graph, AGENT_FLAVOR_ATTR) == "deerflow" + + uninstrument_deerflow() + assert getattr(graph, AGENT_FLAVOR_ATTR) == "langchain-create-agent" + + +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_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..4f69d0229 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py @@ -0,0 +1,485 @@ +# 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 _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_flavor") == "deerflow" + _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_flavor" 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..75cd049f0 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 +### Changed + +- Distinguish LangGraph prebuilt, LangChain `create_agent`, DeepAgents, and + DeerFlow graphs with explicit agent flavors so each framework's model + decision node creates exactly one ReAct `STEP` span. + ## Version 0.7.0 (2026-07-03) ### Added diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/__init__.py index b67577c46..e825f3dcd 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/__init__.py @@ -40,6 +40,11 @@ from wrapt import wrap_function_wrapper from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( + AGENT_FLAVOR_METADATA_KEY, + LANGCHAIN_CREATE_AGENT_FLAVOR, + REACT_AGENT_METADATA_KEY, +) from opentelemetry.instrumentation.langchain.package import _instruments from opentelemetry.instrumentation.utils import unwrap @@ -140,20 +145,19 @@ def _uninstrument_agent_executor() -> None: # create_agent patch (langchain >= 1.x) # ------------------------------------------------------------------ -_REACT_AGENT_GRAPH_ATTR = "_loongsuite_react_agent" - def _create_agent_wrapper( wrapped: Any, _instance: Any, args: Any, kwargs: Any ) -> Any: """``wrapt`` wrapper for ``create_agent``. - Calls the original factory, then marks the returned graph with - ``_loongsuite_react_agent = True`` so that the langgraph Pregel - stream/astream wrapper can inject metadata into ``RunnableConfig``. + Calls the original factory, then marks the returned graph as a LangChain + ``create_agent`` graph. The legacy boolean is retained for compatibility + with older LangGraph instrumentation. """ graph = wrapped(*args, **kwargs) - setattr(graph, _REACT_AGENT_GRAPH_ATTR, True) + setattr(graph, AGENT_FLAVOR_METADATA_KEY, LANGCHAIN_CREATE_AGENT_FLAVOR) + setattr(graph, REACT_AGENT_METADATA_KEY, True) return graph diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_flavor.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_flavor.py new file mode 100644 index 000000000..5552cba49 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_flavor.py @@ -0,0 +1,60 @@ +# 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 contract for distinguishing LangGraph-based agent harnesses.""" + +from __future__ import annotations + +from typing import Any + +AGENT_FLAVOR_METADATA_KEY = "_loongsuite_agent_flavor" +REACT_AGENT_METADATA_KEY = "_loongsuite_react_agent" +DEEPAGENTS_METADATA_KEY = "_loongsuite_deepagents_agent" + +LANGGRAPH_PREBUILT_AGENT_FLAVOR = "langgraph-prebuilt" +LANGCHAIN_CREATE_AGENT_FLAVOR = "langchain-create-agent" +DEEPAGENTS_AGENT_FLAVOR = "deepagents" +DEERFLOW_AGENT_FLAVOR = "deerflow" + +AGENT_FLAVOR_STEP_NODES = { + LANGGRAPH_PREBUILT_AGENT_FLAVOR: "agent", + LANGCHAIN_CREATE_AGENT_FLAVOR: "model", + DEEPAGENTS_AGENT_FLAVOR: "model", + DEERFLOW_AGENT_FLAVOR: "model", +} +SUPPORTED_AGENT_FLAVORS = frozenset(AGENT_FLAVOR_STEP_NODES) + + +def get_agent_flavor(run: Any) -> str | None: + """Return the single agent flavor carried by a callback run. + + The explicit flavor is authoritative. The boolean markers are read only as + compatibility fallbacks for graphs produced by older instrumentation. + """ + + metadata = getattr(run, "metadata", None) or {} + explicit_flavor = metadata.get(AGENT_FLAVOR_METADATA_KEY) + if explicit_flavor is not None: + return ( + explicit_flavor + if isinstance(explicit_flavor, str) + and explicit_flavor in SUPPORTED_AGENT_FLAVORS + else None + ) + + if metadata.get(DEEPAGENTS_METADATA_KEY): + return DEEPAGENTS_AGENT_FLAVOR + if metadata.get(REACT_AGENT_METADATA_KEY): + return LANGGRAPH_PREBUILT_AGENT_FLAVOR + return None 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..696b2606e 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,11 +48,15 @@ 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_flavor import ( + AGENT_FLAVOR_STEP_NODES, + DEERFLOW_AGENT_FLAVOR, + get_agent_flavor, +) from opentelemetry.instrumentation.langchain.internal._utils import ( - DEEPAGENTS_REACT_STEP_NODE, - LANGGRAPH_REACT_STEP_NODE, _documents_to_retrieval_documents, _extract_finish_reasons, _extract_invocation_params, @@ -64,7 +68,6 @@ _extract_token_usage, _extract_tool_definitions, _has_deepagents_metadata, - _has_langgraph_react_metadata, _is_agent_run, _safe_json, ) @@ -123,10 +126,8 @@ class _RunData: react_round: int = 0 active_step: "_RunData | None" = None original_context: Context | None = None - is_langgraph_react: bool = False - inside_langgraph_react: bool = False - is_deepagents_react: bool = False - inside_deepagents_react: bool = False + agent_flavor: str | None = None + inside_agent_flavor: str | None = None deepagents_skills_by_path: dict[str, dict[str, Any]] = field( default_factory=dict ) @@ -354,22 +355,20 @@ def _on_chain_start(self, run: Run) -> None: try: if _is_agent_run(run): self._start_agent(run) - elif _has_langgraph_react_metadata( - run - ) or _has_deepagents_metadata(run): - self._handle_react_chain_start(run) + elif get_agent_flavor(run) is not None: + self._handle_agent_chain_start(run) else: self._start_chain(run) except Exception: logger.debug("Failed to start Chain/Agent span", exc_info=True) - def _handle_react_chain_start(self, run: Run) -> None: - """Route a chain start that carries LoongSuite ReAct metadata. + def _handle_agent_chain_start(self, run: Run) -> None: + """Route a chain start that carries LoongSuite agent flavor metadata. Because ``config["metadata"]`` propagates to child callbacks, both the graph-level run and its child nodes carry the flag. We disambiguate by checking whether any ancestor is a marked - ReAct-style agent or inside one: + flavored agent or inside one: * **Inside marked agent** → child node (chain span, with possible ReAct step transition). @@ -380,23 +379,19 @@ def _handle_react_chain_start(self, run: Run) -> None: parent_rd = self._runs.get(parent_id) if parent_id else None inside = parent_rd is not None and ( - parent_rd.is_langgraph_react - or parent_rd.inside_langgraph_react - or parent_rd.is_deepagents_react - or parent_rd.inside_deepagents_react + parent_rd.agent_flavor is not None + or parent_rd.inside_agent_flavor is not None ) if inside: - if parent_rd is not None and parent_rd.is_deepagents_react: - self._maybe_enter_deepagents_react_step(run) - elif parent_rd is not None and parent_rd.is_langgraph_react: - self._maybe_enter_langgraph_react_step(run) + if parent_rd is not None and parent_rd.agent_flavor is not None: + self._maybe_enter_agent_step(run, parent_rd.agent_flavor) self._start_chain(run) 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 flavored 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,7 +399,32 @@ def _resolve_langgraph_agent_name(self, run: Run) -> str: over the generic default. """ name = run.name or "" - if not _has_langgraph_react_metadata(run) or name != "LangGraph": + agent_flavor = get_agent_flavor(run) + if agent_flavor == DEERFLOW_AGENT_FLAVOR: + 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 agent_flavor is None or name != "LangGraph": return name parent_id = getattr(run, "parent_run_id", None) @@ -446,21 +466,23 @@ 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_flavor = get_agent_flavor(run) + if agent_flavor == DEERFLOW_AGENT_FLAVOR: + invocation.attributes["gen_ai.framework"] = "deerflow" self._handler.start_invoke_agent(invocation, context=parent_ctx) rd = _RunData( run_kind="agent", span=invocation.span, context=otel_context.get_current() if invocation.span else None, invocation=invocation, - is_langgraph_react=_has_langgraph_react_metadata(run), - is_deepagents_react=_has_deepagents_metadata(run), + agent_flavor=agent_flavor, ) with self._lock: self._runs[run.id] = rd @@ -488,20 +510,16 @@ 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 the agent flavor from parent so that # grandchildren of the graph are also recognised as internal. - inside_lg = False - inside_deepagents = False + inside_agent_flavor = None deepagents_skills_by_path: dict[str, dict[str, Any]] = {} parent_id = getattr(run, "parent_run_id", None) if parent_id: with self._lock: p = self._runs.get(parent_id) if p is not None: - inside_lg = p.is_langgraph_react or p.inside_langgraph_react - inside_deepagents = ( - p.is_deepagents_react or p.inside_deepagents_react - ) + inside_agent_flavor = p.agent_flavor or p.inside_agent_flavor deepagents_skills_by_path = dict(p.deepagents_skills_by_path) rd = _RunData( @@ -509,8 +527,7 @@ def _start_chain(self, run: Run) -> None: span=span, context=ctx, context_token=token, - inside_langgraph_react=inside_lg, - inside_deepagents_react=inside_deepagents, + inside_agent_flavor=inside_agent_flavor, deepagents_skills_by_path=deepagents_skills_by_path, ) with self._lock: @@ -731,14 +748,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_agent_step(self, run: Run, agent_flavor: str) -> None: + """Start a STEP for a direct model decision node of a flavored agent. Must be called **before** ``_start_chain`` so that the chain span is parented under the step span. @@ -749,11 +763,12 @@ def _maybe_enter_langgraph_react_step(self, run: Run) -> None: with self._lock: parent_rd = self._runs.get(parent_id) - if parent_rd is None or not parent_rd.is_langgraph_react: + if parent_rd is None or parent_rd.agent_flavor != agent_flavor: return + step_node = AGENT_FLAVOR_STEP_NODES.get(agent_flavor) chain_name = getattr(run, "name", "") or "" - if chain_name != LANGGRAPH_REACT_STEP_NODE: + if step_node is None or chain_name != step_node: return # End previous step (it had tool_calls since another round started) @@ -762,31 +777,6 @@ def _maybe_enter_langgraph_react_step(self, run: Run) -> None: 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. - """ - 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_deepagents_react: - return - - chain_name = getattr(run, "name", "") or "" - if chain_name != DEEPAGENTS_REACT_STEP_NODE: - return - - if parent_rd.active_step is not None: - self._exit_react_step(parent_id, "tool_calls") - - self._enter_react_step(parent_id) - # ------------------------------------------------------------------ # ReAct Step — called from patch wrapper or callback detection # ------------------------------------------------------------------ diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py index 94f3530dc..be6f48c41 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py @@ -18,6 +18,11 @@ import logging from typing import Any +from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( + DEEPAGENTS_AGENT_FLAVOR, + LANGGRAPH_PREBUILT_AGENT_FLAVOR, + get_agent_flavor, +) from opentelemetry.util.genai.extended_types import RetrievalDocument from opentelemetry.util.genai.types import ( FunctionToolDefinition, @@ -44,9 +49,6 @@ } ) -_LANGGRAPH_REACT_METADATA_KEY = "_loongsuite_react_agent" -_DEEPAGENTS_METADATA_KEY = "_loongsuite_deepagents_agent" - LANGGRAPH_REACT_STEP_NODE = "agent" DEEPAGENTS_REACT_STEP_NODE = "model" @@ -72,8 +74,7 @@ def _has_langgraph_react_metadata(run: Any) -> bool: Note: the metadata propagates to child runs, so the caller must distinguish the top-level graph from child nodes. """ - metadata = getattr(run, "metadata", None) or {} - return bool(metadata.get(_LANGGRAPH_REACT_METADATA_KEY)) + return get_agent_flavor(run) == LANGGRAPH_PREBUILT_AGENT_FLAVOR def _has_deepagents_metadata(run: Any) -> bool: @@ -84,8 +85,7 @@ def _has_deepagents_metadata(run: Any) -> bool: ``"model"`` instead of LangGraph ReAct's ``"agent"`` node, so the tracer uses this separate marker to preserve the framework-specific STEP rule. """ - metadata = getattr(run, "metadata", None) or {} - return bool(metadata.get(_DEEPAGENTS_METADATA_KEY)) + return get_agent_flavor(run) == DEEPAGENTS_AGENT_FLAVOR # --------------------------------------------------------------------------- diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_flavor.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_flavor.py new file mode 100644 index 000000000..eebc736e0 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_flavor.py @@ -0,0 +1,87 @@ +# 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 + +from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( + AGENT_FLAVOR_METADATA_KEY, + AGENT_FLAVOR_STEP_NODES, + DEEPAGENTS_AGENT_FLAVOR, + DEEPAGENTS_METADATA_KEY, + DEERFLOW_AGENT_FLAVOR, + LANGCHAIN_CREATE_AGENT_FLAVOR, + LANGGRAPH_PREBUILT_AGENT_FLAVOR, + REACT_AGENT_METADATA_KEY, + get_agent_flavor, +) + + +def test_explicit_agent_flavor_overrides_legacy_markers(): + run = SimpleNamespace( + metadata={ + AGENT_FLAVOR_METADATA_KEY: LANGCHAIN_CREATE_AGENT_FLAVOR, + REACT_AGENT_METADATA_KEY: True, + DEEPAGENTS_METADATA_KEY: True, + } + ) + + assert get_agent_flavor(run) == LANGCHAIN_CREATE_AGENT_FLAVOR + + +def test_deepagents_marker_precedes_legacy_react_marker(): + run = SimpleNamespace( + metadata={ + REACT_AGENT_METADATA_KEY: True, + DEEPAGENTS_METADATA_KEY: True, + } + ) + + assert get_agent_flavor(run) == DEEPAGENTS_AGENT_FLAVOR + + +def test_legacy_react_marker_maps_to_langgraph_prebuilt(): + run = SimpleNamespace(metadata={REACT_AGENT_METADATA_KEY: True}) + + assert get_agent_flavor(run) == LANGGRAPH_PREBUILT_AGENT_FLAVOR + + +def test_unknown_explicit_agent_flavor_does_not_fall_back(): + run = SimpleNamespace( + metadata={ + AGENT_FLAVOR_METADATA_KEY: "unknown", + REACT_AGENT_METADATA_KEY: True, + } + ) + + assert get_agent_flavor(run) is None + + +def test_non_string_explicit_agent_flavor_does_not_fall_back(): + run = SimpleNamespace( + metadata={ + AGENT_FLAVOR_METADATA_KEY: ["deerflow"], + REACT_AGENT_METADATA_KEY: True, + } + ) + + assert get_agent_flavor(run) is None + + +def test_deerflow_explicit_flavor_uses_model_step_node(): + run = SimpleNamespace( + metadata={AGENT_FLAVOR_METADATA_KEY: DEERFLOW_AGENT_FLAVOR} + ) + + assert get_agent_flavor(run) == DEERFLOW_AGENT_FLAVOR + assert AGENT_FLAVOR_STEP_NODES[DEERFLOW_AGENT_FLAVOR] == "model" 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..5043ec6c1 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,13 @@ from uuid import uuid4 +import pytest + +from opentelemetry import baggage, context +from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( + AGENT_FLAVOR_METADATA_KEY, + DEERFLOW_AGENT_FLAVOR, +) from opentelemetry.instrumentation.langchain.internal._tracer import ( LoongsuiteTracer, _extract_langgraph_input_message, @@ -162,3 +169,88 @@ 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_flavor_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_FLAVOR_METADATA_KEY: DEERFLOW_AGENT_FLAVOR} + + 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_FLAVOR_METADATA_KEY: DEERFLOW_AGENT_FLAVOR, + **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_FLAVOR_METADATA_KEY: DEERFLOW_AGENT_FLAVOR} + 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..134b2db9f 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 +### Changed + +- Propagate explicit agent flavor metadata through `Pregel.stream` and + `Pregel.astream` using copy-on-write config updates while retaining the + legacy ReAct marker fallback. + ## 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..7d11326eb 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 @@ -20,15 +20,13 @@ 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. +1. ``create_react_agent`` — marks the compiled ``CompiledStateGraph`` with the + explicit ``langgraph-prebuilt`` agent flavor. 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. + ``metadata["_loongsuite_agent_flavor"]`` into a copy of the + ``RunnableConfig`` when the graph is a marked agent. This metadata flows + through LangChain's callback system to ``Run.metadata``. """ from __future__ import annotations @@ -38,7 +36,9 @@ def wrapper(wrapped, instance, args, kwargs) -> ... logger = logging.getLogger(__name__) +AGENT_FLAVOR_METADATA_KEY = "_loongsuite_agent_flavor" REACT_AGENT_METADATA_KEY = "_loongsuite_react_agent" +LANGGRAPH_PREBUILT_AGENT_FLAVOR = "langgraph-prebuilt" # --------------------------------------------------------------------------- @@ -51,16 +51,21 @@ def _create_react_agent_wrapper( ) -> Any: """``wrapt`` wrapper for ``create_react_agent``. - Calls the original function, then marks the returned graph with - ``_loongsuite_react_agent = True``. + Calls the original function, then marks the returned graph with its + explicit agent flavor. The legacy boolean remains for compatibility. """ graph = wrapped(*args, **kwargs) + setattr( + graph, + AGENT_FLAVOR_METADATA_KEY, + LANGGRAPH_PREBUILT_AGENT_FLAVOR, + ) setattr(graph, REACT_AGENT_METADATA_KEY, True) logger.debug( "[INSTRUMENTATION] create_react_agent patched graph: name=%r, %s=%r", getattr(graph, "name", None), - REACT_AGENT_METADATA_KEY, - True, + AGENT_FLAVOR_METADATA_KEY, + LANGGRAPH_PREBUILT_AGENT_FLAVOR, ) return graph @@ -70,10 +75,8 @@ 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 _inject_agent_metadata(config: Any, agent_flavor: str) -> Any: + """Return a new config with the graph's single agent flavor metadata.""" # Inline import: langchain_core is a transitive dependency of langgraph; # importing here avoids module-level coupling. from langchain_core.runnables.config import ( # noqa: PLC0415 @@ -83,15 +86,28 @@ def _inject_react_metadata(config: Any) -> Any: config = ensure_config(config) config = {**config} metadata = dict(config.get("metadata") or {}) + metadata[AGENT_FLAVOR_METADATA_KEY] = agent_flavor metadata.setdefault(REACT_AGENT_METADATA_KEY, True) config["metadata"] = metadata return config +def _get_graph_agent_flavor(graph: Any) -> str | None: + """Prefer an explicit graph flavor, falling back to the legacy marker.""" + + explicit_flavor = getattr(graph, AGENT_FLAVOR_METADATA_KEY, None) + if isinstance(explicit_flavor, str) and explicit_flavor: + return explicit_flavor + if getattr(graph, REACT_AGENT_METADATA_KEY, False): + return LANGGRAPH_PREBUILT_AGENT_FLAVOR + return None + + 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): - args, kwargs = _rewrite_config(args, kwargs) + agent_flavor = _get_graph_agent_flavor(instance) + if agent_flavor is not None: + args, kwargs = _rewrite_config(args, kwargs, agent_flavor) yield from wrapped(*args, **kwargs) @@ -99,20 +115,23 @@ 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): - args, kwargs = _rewrite_config(args, kwargs) + agent_flavor = _get_graph_agent_flavor(instance) + if agent_flavor is not None: + args, kwargs = _rewrite_config(args, kwargs, agent_flavor) 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], + agent_flavor: str, ) -> 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_metadata(args[1], agent_flavor) args = (args[0], config) + args[2:] else: - config = _inject_react_metadata(kwargs.get("config")) + config = _inject_agent_metadata(kwargs.get("config"), agent_flavor) 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..4e70e7ac7 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_patch.py @@ -25,6 +25,12 @@ from langchain_core.tools import tool from opentelemetry.instrumentation.langgraph import LangGraphInstrumentor +from opentelemetry.instrumentation.langgraph.internal.patch import ( + AGENT_FLAVOR_METADATA_KEY, + LANGGRAPH_PREBUILT_AGENT_FLAVOR, + _get_graph_agent_flavor, + _inject_agent_metadata, +) # --------------------------------------------------------------------------- # Helpers @@ -72,6 +78,10 @@ def test_patch_adds_react_flag(self, instrument): """The compiled graph has ``_loongsuite_react_agent = True``.""" graph = _build_react_agent() assert getattr(graph, "_loongsuite_react_agent", False) is True + assert ( + getattr(graph, AGENT_FLAVOR_METADATA_KEY) + == LANGGRAPH_PREBUILT_AGENT_FLAVOR + ) def test_patch_does_not_override_default_name(self, instrument): """The original graph name is preserved when no explicit name @@ -86,6 +96,10 @@ def test_patch_preserves_explicit_name(self, instrument): graph = _build_react_agent(name="my_custom_agent") assert graph.name == "my_custom_agent" assert getattr(graph, "_loongsuite_react_agent", False) is True + assert ( + getattr(graph, AGENT_FLAVOR_METADATA_KEY) + == LANGGRAPH_PREBUILT_AGENT_FLAVOR + ) def test_uninstrument_restores_original(self): """After ``uninstrument()``, ``create_react_agent`` returns an @@ -97,6 +111,7 @@ def test_uninstrument_restores_original(self): graph = _build_react_agent() assert not getattr(graph, "_loongsuite_react_agent", False) + assert not hasattr(graph, AGENT_FLAVOR_METADATA_KEY) def test_double_instrument_is_safe(self): """Calling ``instrument()`` twice does not break the wrapper.""" @@ -156,6 +171,36 @@ 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_agent_metadata( + original, LANGGRAPH_PREBUILT_AGENT_FLAVOR + ) + + 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"][AGENT_FLAVOR_METADATA_KEY] + == LANGGRAPH_PREBUILT_AGENT_FLAVOR + ) + + def test_explicit_flavor_does_not_require_legacy_marker(self): + graph = type("Graph", (), {})() + setattr(graph, AGENT_FLAVOR_METADATA_KEY, "deerflow") + + assert _get_graph_agent_flavor(graph) == "deerflow" + 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/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deerflow.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deerflow.py new file mode 100644 index 000000000..72a7461b1 --- /dev/null +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deerflow.py @@ -0,0 +1,28 @@ +# 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. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. +# +# Generated with options: +# --upstream-version: (from source) +# --loongsuite-version: (from source) + +REGISTRY = { + "source": "loongsuite", + "package": "loongsuite-instrumentation-deerflow", + "instrumentation": "loongsuite-instrumentation-deerflow==0.8.0.dev", + "libraries": [], + "default": True, +} diff --git a/tox-loongsuite.ini b/tox-loongsuite.ini index 6482b0196..03ddb7377 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,13 @@ deps = lint-loongsuite-instrumentation-deepagents: {[testenv]test_deps} lint-loongsuite-instrumentation-deepagents: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/requirements.latest.txt + 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: {[testenv]test_deps} + loongsuite-instrumentation-deerflow.current: -r {toxinidir}/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/requirements.latest.txt + 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 +335,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") From d8182d944b51dd095a1f14559fb080707f330f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= <53717475+sipercai@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:47:30 +0800 Subject: [PATCH 2/6] chore: refresh LoongSuite bootstrap versions --- .../bootstrap_registry/loongsuite_instrumentation_agentscope.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_agno.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_algotune.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_autogen.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_bfclv4.py | 2 +- .../loongsuite_instrumentation_claude_agent_sdk.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_claw_eval.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_crewai.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_dashscope.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_deepagents.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_dify.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_google_adk.py | 2 +- .../loongsuite_instrumentation_hermes_agent.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_langchain.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_langgraph.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_litellm.py | 2 +- .../distro/bootstrap_registry/loongsuite_instrumentation_mcp.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_mem0.py | 2 +- .../loongsuite_instrumentation_microsoft_agent_framework.py | 2 +- .../loongsuite_instrumentation_minisweagent.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_openhands.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_qwen_agent.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_qwenpaw.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_slop_code.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_terminus2.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_vita.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_webarena.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_widesearch.py | 2 +- .../bootstrap_registry/loongsuite_instrumentation_wildtool.py | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py index 31ba7075b..39e84423c 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-agentscope", - "instrumentation": "loongsuite-instrumentation-agentscope==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-agentscope==0.8.0.dev", "libraries": [ "agentscope >= 1.0.0, < 3.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py index 8900567ca..87bc4c5f9 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-agno", - "instrumentation": "loongsuite-instrumentation-agno==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-agno==0.8.0.dev", "libraries": [ "agno >= 2.0.0, < 3", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py index 4d19cf800..9c05ad93e 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-algotune", - "instrumentation": "loongsuite-instrumentation-algotune==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-algotune==0.8.0.dev", "libraries": [], "default": True, } diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py index fad1a340b..bdef66846 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-autogen", - "instrumentation": "loongsuite-instrumentation-autogen==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-autogen==0.8.0.dev", "libraries": [ "autogen-agentchat >= 0.7.0, < 0.8.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py index 6413bfa83..d035dcd97 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-bfclv4", - "instrumentation": "loongsuite-instrumentation-bfclv4==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-bfclv4==0.8.0.dev", "libraries": [ "bfcl-eval >= 4.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py index 1e35bc2bc..10a019424 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-claude-agent-sdk", - "instrumentation": "loongsuite-instrumentation-claude-agent-sdk==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-claude-agent-sdk==0.8.0.dev", "libraries": [ "claude-agent-sdk >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py index 9fa624b93..d31178055 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-claw-eval", - "instrumentation": "loongsuite-instrumentation-claw-eval==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-claw-eval==0.8.0.dev", "libraries": [ "claw-eval >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py index c0884657c..107449a43 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-crewai", - "instrumentation": "loongsuite-instrumentation-crewai==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-crewai==0.8.0.dev", "libraries": [ "crewai >= 0.80.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py index bd4b7dd04..87bf8946b 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-dashscope", - "instrumentation": "loongsuite-instrumentation-dashscope==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-dashscope==0.8.0.dev", "libraries": [ "dashscope >= 1.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py index b22695004..aa9574de5 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-deepagents", - "instrumentation": "loongsuite-instrumentation-deepagents==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-deepagents==0.8.0.dev", "libraries": [ "deepagents >= 0.6.0, < 0.7.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py index c62b18d1d..e48cbea48 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-dify", - "instrumentation": "loongsuite-instrumentation-dify==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-dify==0.8.0.dev", "libraries": [], "default": True, } diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py index eae6d6912..18c16a789 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-google-adk", - "instrumentation": "loongsuite-instrumentation-google-adk==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-google-adk==0.8.0.dev", "libraries": [ "google-adk >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py index 47406ffd8..696112813 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-hermes-agent", - "instrumentation": "loongsuite-instrumentation-hermes-agent==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-hermes-agent==0.8.0.dev", "libraries": [ "openai >= 1.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py index d68d28959..0f4b57f3c 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-langchain", - "instrumentation": "loongsuite-instrumentation-langchain==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-langchain==0.8.0.dev", "libraries": [ "langchain_core >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py index 2b52ff124..be176f920 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-langgraph", - "instrumentation": "loongsuite-instrumentation-langgraph==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-langgraph==0.8.0.dev", "libraries": [ "langgraph >= 0.2", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py index eeca638e5..cb9471392 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-litellm", - "instrumentation": "loongsuite-instrumentation-litellm==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-litellm==0.8.0.dev", "libraries": [ "litellm >= 1.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py index a293ee1db..3c86370be 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-mcp", - "instrumentation": "loongsuite-instrumentation-mcp==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-mcp==0.8.0.dev", "libraries": [ "mcp >= 1.3.0, <= 1.25.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py index f20839dd3..90bf7b7b4 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-mem0", - "instrumentation": "loongsuite-instrumentation-mem0==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-mem0==0.8.0.dev", "libraries": [ "mem0ai >= 1.0.0, < 2.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py index b6416e669..2aeee9ef4 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-microsoft-agent-framework", - "instrumentation": "loongsuite-instrumentation-microsoft-agent-framework==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-microsoft-agent-framework==0.8.0.dev", "libraries": [ "agent-framework-core >= 1.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py index 10349e1d3..1945a10f9 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-minisweagent", - "instrumentation": "loongsuite-instrumentation-minisweagent==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-minisweagent==0.8.0.dev", "libraries": [ "mini-swe-agent >= 2.2.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py index adbaa570b..20b60af35 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-openhands", - "instrumentation": "loongsuite-instrumentation-openhands==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-openhands==0.8.0.dev", "libraries": [], "default": True, } diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py index cb087d592..952e19150 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-qwen-agent", - "instrumentation": "loongsuite-instrumentation-qwen-agent==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-qwen-agent==0.8.0.dev", "libraries": [ "qwen-agent >= 0.0.20", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py index c244de352..d8aba3247 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-qwenpaw", - "instrumentation": "loongsuite-instrumentation-qwenpaw==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-qwenpaw==0.8.0.dev", "libraries": [ "qwenpaw >= 1.1.0", "copaw >= 0.1.0, <= 1.0.2", diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py index 2084fe6fc..2abb399fa 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-slop-code", - "instrumentation": "loongsuite-instrumentation-slop-code==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-slop-code==0.8.0.dev", "libraries": [ "slop-code-bench >= 0.1", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py index 09ce12c09..3711b6e59 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-terminus2", - "instrumentation": "loongsuite-instrumentation-terminus2==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-terminus2==0.8.0.dev", "libraries": [ "terminal-bench >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py index be49f87b2..62bf2376b 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-vita", - "instrumentation": "loongsuite-instrumentation-vita==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-vita==0.8.0.dev", "libraries": [ "vita >= 0.0.1", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py index dae9a5933..dc4cf4a7a 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-webarena", - "instrumentation": "loongsuite-instrumentation-webarena==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-webarena==0.8.0.dev", "libraries": [ "webarena >= 0.0.1", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py index b1e846631..bdc6f1e20 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-widesearch", - "instrumentation": "loongsuite-instrumentation-widesearch==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-widesearch==0.8.0.dev", "libraries": [ "widesearch >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py index 703fb6e2b..ff5ace5ca 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-wildtool", - "instrumentation": "loongsuite-instrumentation-wildtool==0.7.0.dev", + "instrumentation": "loongsuite-instrumentation-wildtool==0.8.0.dev", "libraries": [ "openai >= 1.0.0", ], From bb2a3ecd4cbf5589804b7244dfdd417b8e10fd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= <53717475+sipercai@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:09:00 +0800 Subject: [PATCH 3/6] fix: rollback partial DeerFlow setup --- .../instrumentation/deerflow/__init__.py | 52 +++++------ .../tests/test_instrumentor.py | 92 +++++++++++++++++++ 2 files changed, 118 insertions(+), 26 deletions(-) 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 index 7422cea31..a8b8b1c29 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/__init__.py @@ -132,34 +132,34 @@ def _instrument(self, **kwargs: Any) -> None: if not _deerflow_runtime_supported(): return - 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) + 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, - ) + 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"), - ) - try: + 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() diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py index d655f70ef..7ce6c763a 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py @@ -19,9 +19,16 @@ import importlib import inspect 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, @@ -49,6 +56,20 @@ def test_has_no_pypi_target_library_dependency(): 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 + + @pytest.mark.parametrize( ("installed_version", "supported"), [ @@ -97,6 +118,77 @@ def fail_import(_name): 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_create_agent_alias_marks_only_new_flavor(): graph = _Graph() graph._loongsuite_react_agent = True From 7830132df14513fb2b4f22473148a4f1b7dbffe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= <53717475+sipercai@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:54:28 +0800 Subject: [PATCH 4/6] fix: refine DeerFlow interruption lifecycle --- .../CHANGELOG.md | 6 ++ .../README.md | 8 +- .../instrumentation/deerflow/__init__.py | 27 ++++++- .../deerflow/internal/patch.py | 35 ++++++++- .../tests/test_entry_wrappers.py | 74 +++++++++++++++++-- .../tests/test_instrumentor.py | 66 +++++++++++++++++ 6 files changed, 200 insertions(+), 16 deletions(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md index bc5f56d4e..109e831db 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 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. + ## Version 0.8.0 (2026-07-14) ### Added diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md index 0bb81fb77..6cdfa9f61 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md @@ -55,8 +55,12 @@ expose its internally generated run id. Message content is disabled by default and follows the existing experimental GenAI content-capture switch. -`task` remains a normal LangChain tool. This package does not patch DeerFlow's -sandbox, memory, skills, or context propagation internals. +`task` remains a normal LangChain tool. Operations performed in a sandbox are +visible when DeerFlow exposes them as LangChain tools, and work selected by a +skill remains visible through its downstream LLM and tool calls. This package +does not create separate lifecycle spans for sandbox allocation or reuse, skill +activation, or background memory work, and it does not patch DeerFlow's context +propagation internals. ## Compatibility 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 index a8b8b1c29..ee209f666 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/__init__.py @@ -25,6 +25,7 @@ from opentelemetry.instrumentation.deerflow.internal.patch import ( instrument_deerflow, + remove_owned_langchain_alias_wrappers, uninstrument_deerflow, ) from opentelemetry.instrumentation.deerflow.package import _instruments @@ -39,6 +40,13 @@ _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: @@ -163,8 +171,14 @@ def _instrument(self, **kwargs: Any) -> None: 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 @@ -174,9 +188,14 @@ def _uninstrument(self, **kwargs: Any) -> None: uninstrument_deerflow() self._deerflow_patched = False - for instrumentor in reversed( - getattr(self, "_dependency_instrumentors", []) - ): + 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 @@ -185,4 +204,6 @@ def _uninstrument(self, **kwargs: Any) -> None: 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/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/patch.py index 8e111d834..b2465eb68 100644 --- 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 @@ -365,7 +365,10 @@ async def __call__( else "error" ) invocation.attributes[DEERFLOW_RUN_STATUS] = status - _safe_fail_entry(self._handler, invocation, exc) + if status == "interrupted": + _safe_stop_entry(self._handler, invocation) + else: + _safe_fail_entry(self._handler, invocation, exc) raise else: status = _run_status_value(record) @@ -374,7 +377,7 @@ async def __call__( invocation.output_messages = to_output_messages( getattr(record, "last_ai_message", None) ) - if status == "success": + if status in {"success", "interrupted"}: _safe_stop_entry(self._handler, invocation) else: _safe_fail_entry( @@ -457,7 +460,10 @@ def _finish_isolated_entry( else: status = "error" invocation.attributes[DEERFLOW_RUN_STATUS] = status - _safe_fail_entry(handler, invocation, error) + 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) @@ -729,6 +735,29 @@ def _restore_late_gateway_aliases() -> None: ) +def remove_owned_langchain_alias_wrappers() -> None: + """Remove cached LangChain wrappers after DeerFlow-owned teardown.""" + 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() diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py index 8c8ce616f..54b889ca2 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py @@ -103,7 +103,13 @@ def stream(message, *, thread_id=None, **_kwargs): first = next(result) assert first.data["content"] == "hello " assert not trace.get_current_span().get_span_context().is_valid - assert list(result)[0].data["content"] == "world" + 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" @@ -112,6 +118,8 @@ def stream(message, *, thread_id=None, **_kwargs): 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" @@ -133,6 +141,26 @@ def stream(message, *, thread_id=None, **_kwargs): ) +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, @@ -219,7 +247,7 @@ def test_default_client_does_not_invent_assistant_id(handler, span_exporter): assert "deerflow.assistant.id" not in entry.attributes -def test_client_stream_close_fails_entry_and_closes_inner( +def test_client_stream_close_interrupts_entry_without_error( handler, span_exporter, ): @@ -240,8 +268,8 @@ def stream(_message, *, thread_id=None): 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.ERROR - assert entry.attributes["error.type"] == "GeneratorExit" + assert entry.status.status_code == StatusCode.UNSET + assert "error.type" not in entry.attributes assert entry.attributes[DEERFLOW_RUN_STATUS] == "interrupted" @@ -647,7 +675,6 @@ async def run_agent(_bridge, _manager, mutable_record, **_kwargs): [ ("error", "DeerFlowRunError"), ("timeout", "DeerFlowRunTimeout"), - ("interrupted", "DeerFlowRunInterrupted"), ], ) async def test_gateway_terminal_status_fails_entry( @@ -683,7 +710,38 @@ async def run_agent(_bridge, _manager, mutable_record, **_kwargs): @pytest.mark.asyncio -async def test_gateway_cancellation_fails_entry_as_interrupted( +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, ): @@ -709,8 +767,8 @@ async def run_agent(_bridge, _manager, mutable_record, **_kwargs): ) entry = _entry_spans(span_exporter)[0] - assert entry.status.status_code == StatusCode.ERROR - assert entry.attributes["error.type"] == "CancelledError" + assert entry.status.status_code == StatusCode.UNSET + assert "error.type" not in entry.attributes assert entry.attributes[DEERFLOW_RUN_STATUS] == "interrupted" diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py index 7ce6c763a..4050c2508 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py @@ -18,6 +18,7 @@ import importlib import inspect +import subprocess import sys from importlib.metadata import ( PackageNotFoundError, @@ -70,6 +71,71 @@ def test_official_source_distribution_and_gateway_signature_are_supported(): 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"), [ From ba044f3f17692b4a83bf3d2eb1337ace56e4375d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= <53717475+sipercai@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:17:56 +0800 Subject: [PATCH 5/6] test: align DeerFlow tox with OTel development stack --- .../tests/test_runtime_graph.py | 4 ++++ tox-loongsuite.ini | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py index 4f69d0229..93a225f04 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py @@ -142,6 +142,10 @@ def __init__(self) -> 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( diff --git a/tox-loongsuite.ini b/tox-loongsuite.ini index 03ddb7377..df6c6fb70 100644 --- a/tox-loongsuite.ini +++ b/tox-loongsuite.ini @@ -205,10 +205,21 @@ 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 From 8c70b09b05a7081ea4c1efbcf61303793607a64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Wed, 22 Jul 2026 11:08:21 +0800 Subject: [PATCH 6/6] fix: narrow DeerFlow observability integration --- instrumentation-loongsuite/README.md | 4 +- .../CHANGELOG.md | 6 - .../deepagents/internal/patch.py | 13 +- .../tests/test_deepagents_spans.py | 149 --------- .../tests/test_instrumentor.py | 20 -- .../CHANGELOG.md | 4 + .../README.md | 315 +++++++++++++++--- .../deerflow/internal/constants.py | 6 +- .../deerflow/internal/patch.py | 49 ++- .../tests/test_entry_wrappers.py | 67 ++++ .../tests/test_instrumentor.py | 122 ++++++- .../tests/test_runtime_graph.py | 6 +- .../CHANGELOG.md | 8 +- .../instrumentation/langchain/__init__.py | 16 +- .../langchain/internal/_agent_flavor.py | 60 ---- .../langchain/internal/_agent_semantics.py | 36 ++ .../langchain/internal/_tracer.py | 158 +++++++-- .../langchain/internal/_utils.py | 14 +- .../tests/test_agent_flavor.py | 87 ----- .../tests/test_agent_semantics.py | 54 +++ .../tests/test_agent_spans.py | 22 +- .../CHANGELOG.md | 8 +- .../langgraph/internal/patch.py | 97 ++++-- .../tests/test_patch.py | 57 ++-- .../tests/test_react_step_spans.py | 61 +++- .../loongsuite_instrumentation_agentscope.py | 2 +- .../loongsuite_instrumentation_agno.py | 2 +- .../loongsuite_instrumentation_algotune.py | 2 +- .../loongsuite_instrumentation_autogen.py | 2 +- .../loongsuite_instrumentation_bfclv4.py | 2 +- ...gsuite_instrumentation_claude_agent_sdk.py | 2 +- .../loongsuite_instrumentation_claw_eval.py | 2 +- .../loongsuite_instrumentation_crewai.py | 2 +- .../loongsuite_instrumentation_dashscope.py | 2 +- .../loongsuite_instrumentation_deepagents.py | 2 +- .../loongsuite_instrumentation_deerflow.py | 28 -- .../loongsuite_instrumentation_dify.py | 2 +- .../loongsuite_instrumentation_google_adk.py | 2 +- ...loongsuite_instrumentation_hermes_agent.py | 2 +- .../loongsuite_instrumentation_langchain.py | 2 +- .../loongsuite_instrumentation_langgraph.py | 2 +- .../loongsuite_instrumentation_litellm.py | 2 +- .../loongsuite_instrumentation_mcp.py | 2 +- .../loongsuite_instrumentation_mem0.py | 2 +- ...strumentation_microsoft_agent_framework.py | 2 +- ...loongsuite_instrumentation_minisweagent.py | 2 +- .../loongsuite_instrumentation_openhands.py | 2 +- .../loongsuite_instrumentation_qwen_agent.py | 2 +- .../loongsuite_instrumentation_qwenpaw.py | 2 +- .../loongsuite_instrumentation_slop_code.py | 2 +- .../loongsuite_instrumentation_terminus2.py | 2 +- .../loongsuite_instrumentation_vita.py | 2 +- .../loongsuite_instrumentation_webarena.py | 2 +- .../loongsuite_instrumentation_widesearch.py | 2 +- .../loongsuite_instrumentation_wildtool.py | 2 +- 55 files changed, 944 insertions(+), 581 deletions(-) delete mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_flavor.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_semantics.py delete mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_flavor.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_semantics.py delete mode 100644 loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deerflow.py diff --git a/instrumentation-loongsuite/README.md b/instrumentation-loongsuite/README.md index 39f58cc71..07bd2ab02 100644 --- a/instrumentation-loongsuite/README.md +++ b/instrumentation-loongsuite/README.md @@ -11,7 +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 | 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 @@ -30,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-deepagents/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/CHANGELOG.md index eb3dbf67f..0c4bf074b 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/CHANGELOG.md @@ -7,12 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -### Changed - -- Mark DeepAgents graphs with the explicit `deepagents` agent flavor so their - `model` nodes remain the sole ReAct `STEP` boundary when multiple compatible - instrumentors are enabled. - ## Version 0.7.0 (2026-07-03) ### Added diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/patch.py index c9a06bcc2..ee78ba8e4 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/patch.py @@ -16,9 +16,10 @@ DeepAgents builds on ``langchain.agents.create_agent`` and returns ``create_agent(...).with_config(...)``. That final ``with_config`` call creates -a new graph object and drops the flavor that LangChain instrumentation places -on the original graph. This module marks the returned graph as ``deepagents`` -and injects that flavor into call-time config. +a new graph object and drops the marker that LangChain instrumentation places +on the original graph. This module restores the marker on the returned graph +and injects the marker into call-time config, mirroring LangGraph's lightweight +metadata handoff. """ from __future__ import annotations @@ -39,8 +40,6 @@ CREATE_DEEP_AGENT_MODULE = "deepagents.graph" CREATE_DEEP_AGENT_NAME = "create_deep_agent" -AGENT_FLAVOR_METADATA_KEY = "_loongsuite_agent_flavor" -DEEPAGENTS_AGENT_FLAVOR = "deepagents" REACT_AGENT_METADATA_KEY = "_loongsuite_react_agent" DEEPAGENTS_METADATA_KEY = "_loongsuite_deepagents_agent" GRAPH_METHODS_WRAPPED_ATTR = "_loongsuite_deepagents_methods_wrapped" @@ -182,7 +181,6 @@ def _restore_top_level_create_deep_agent() -> None: def _mark_graph(graph: Any) -> None: with suppress(Exception): - setattr(graph, AGENT_FLAVOR_METADATA_KEY, DEEPAGENTS_AGENT_FLAVOR) setattr(graph, REACT_AGENT_METADATA_KEY, True) setattr(graph, DEEPAGENTS_METADATA_KEY, True) @@ -243,7 +241,6 @@ def _restore_graph_methods(graph: Any) -> None: for attr_name in ( GRAPH_ORIGINAL_METHODS_ATTR, GRAPH_METHODS_WRAPPED_ATTR, - AGENT_FLAVOR_METADATA_KEY, REACT_AGENT_METADATA_KEY, DEEPAGENTS_METADATA_KEY, ): @@ -316,8 +313,6 @@ def _inject_react_metadata(config: Any) -> Any: config = ensure_config(config) config = {**config} metadata = dict(config.get("metadata") or {}) - metadata[AGENT_FLAVOR_METADATA_KEY] = DEEPAGENTS_AGENT_FLAVOR - metadata.setdefault(REACT_AGENT_METADATA_KEY, True) metadata.setdefault(DEEPAGENTS_METADATA_KEY, True) config["metadata"] = metadata return config diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_deepagents_spans.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_deepagents_spans.py index 8174a2b7b..e0b45cd51 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_deepagents_spans.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_deepagents_spans.py @@ -28,12 +28,6 @@ from langchain_core.outputs import ChatGeneration, ChatResult from langchain_core.tools import tool -from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( - AGENT_FLAVOR_METADATA_KEY, - DEEPAGENTS_AGENT_FLAVOR, - LANGCHAIN_CREATE_AGENT_FLAVOR, -) - class _SequenceToolCallingModel(BaseChatModel): def __init__(self, responses: list[AIMessage]): @@ -162,148 +156,6 @@ def _assert_kind_under_step( return matches -def test_langchain_create_agent_model_node_creates_single_step( - instrument, span_exporter -): - """LangChain 1.x ``create_agent`` uses ``model``, not ``agent``, nodes.""" - from langchain.agents import create_agent # noqa: PLC0415 - - agent = create_agent( - model=_SequenceToolCallingModel( - [AIMessage(content="Plain LangChain final answer.")] - ), - tools=[lookup_city], - name="plain_langchain_agent", - ) - - assert ( - getattr(agent, AGENT_FLAVOR_METADATA_KEY) - == LANGCHAIN_CREATE_AGENT_FLAVOR - ) - assert not hasattr(agent, "_loongsuite_deepagents_agent") - - result = agent.invoke({"messages": [{"role": "user", "content": "hello"}]}) - - assert result["messages"][-1].content == "Plain LangChain final answer." - spans = span_exporter.get_finished_spans() - agent_span = _assert_single_agent(spans, "plain_langchain_agent") - step_spans = _spans_by_kind(spans, "STEP") - assert len(step_spans) == 1 - _assert_step_is_under_agent(agent_span, step_spans[0]) - - -def test_langchain_create_agent_tool_loop_creates_two_steps( - instrument, span_exporter -): - from langchain.agents import create_agent # noqa: PLC0415 - - agent = create_agent( - model=_SequenceToolCallingModel( - [ - AIMessage( - content="", - tool_calls=[ - { - "name": "lookup_city", - "args": {"query": "where"}, - "id": "plain_call_1", - } - ], - ), - AIMessage(content="Plain answer: Hangzhou."), - ] - ), - tools=[lookup_city], - name="plain_langchain_tool_agent", - ) - - result = agent.invoke({"messages": [{"role": "user", "content": "where"}]}) - - assert result["messages"][-1].content == "Plain answer: Hangzhou." - spans = span_exporter.get_finished_spans() - _assert_single_agent(spans, "plain_langchain_tool_agent") - step_spans = sorted( - _spans_by_kind(spans, "STEP"), - key=lambda span: span.attributes.get("gen_ai.react.round", 0), - ) - assert len(step_spans) == 2 - assert [ - span.attributes.get("gen_ai.react.finish_reason") - for span in step_spans - ] == ["tool_calls", "stop"] - - -def test_langchain_create_agent_stream_creates_single_step( - instrument, - span_exporter, -): - from langchain.agents import create_agent # noqa: PLC0415 - - agent = create_agent( - model=_SequenceToolCallingModel( - [AIMessage(content="Plain streamed answer.")] - ), - tools=[lookup_city], - name="plain_langchain_stream_agent", - ) - - chunks = list( - agent.stream({"messages": [{"role": "user", "content": "hello"}]}) - ) - - assert chunks - spans = span_exporter.get_finished_spans() - agent_span = _assert_single_agent(spans, "plain_langchain_stream_agent") - step_spans = _spans_by_kind(spans, "STEP") - assert len(step_spans) == 1 - _assert_step_is_under_agent(agent_span, step_spans[0]) - - -@pytest.mark.asyncio -async def test_langchain_create_agent_async_tool_loop_creates_two_steps( - instrument, - span_exporter, -): - from langchain.agents import create_agent # noqa: PLC0415 - - agent = create_agent( - model=_SequenceToolCallingModel( - [ - AIMessage( - content="", - tool_calls=[ - { - "name": "lookup_city", - "args": {"query": "where"}, - "id": "plain_async_call_1", - } - ], - ), - AIMessage(content="Async answer: Hangzhou."), - ] - ), - tools=[lookup_city], - name="plain_langchain_async_agent", - ) - - result = await agent.ainvoke( - {"messages": [{"role": "user", "content": "where"}]} - ) - - assert result["messages"][-1].content == "Async answer: Hangzhou." - spans = span_exporter.get_finished_spans() - _assert_single_agent(spans, "plain_langchain_async_agent") - step_spans = sorted( - _spans_by_kind(spans, "STEP"), - key=lambda span: span.attributes.get("gen_ai.react.round", 0), - ) - assert len(step_spans) == 2 - assert [ - span.attributes.get("gen_ai.react.finish_reason") - for span in step_spans - ] == ["tool_calls", "stop"] - - def test_deepagents_root_span_is_agent_and_single_step( instrument, span_exporter ): @@ -314,7 +166,6 @@ def test_deepagents_root_span_is_agent_and_single_step( assert getattr(agent, "_loongsuite_react_agent") is True assert getattr(agent, "_loongsuite_deepagents_agent") is True - assert getattr(agent, AGENT_FLAVOR_METADATA_KEY) == DEEPAGENTS_AGENT_FLAVOR result = agent.invoke({"messages": [{"role": "user", "content": "hello"}]}) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_instrumentor.py b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_instrumentor.py index 96ef8ce3a..cf048f099 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_instrumentor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deepagents/tests/test_instrumentor.py @@ -18,8 +18,6 @@ from opentelemetry.instrumentation.deepagents import DeepAgentsInstrumentor from opentelemetry.instrumentation.deepagents.internal.patch import ( - AGENT_FLAVOR_METADATA_KEY, - DEEPAGENTS_AGENT_FLAVOR, DEEPAGENTS_METADATA_KEY, GRAPH_METHODS_WRAPPED_ATTR, GRAPH_ORIGINAL_METHODS_ATTR, @@ -101,18 +99,10 @@ def test_uninstrument_restores_wrapped_graph_methods(): _wrap_graph_methods(graph) assert getattr(graph, GRAPH_METHODS_WRAPPED_ATTR) is True - assert ( - getattr(graph, AGENT_FLAVOR_METADATA_KEY) - == DEEPAGENTS_AGENT_FLAVOR - ) assert getattr(graph, REACT_AGENT_METADATA_KEY) is True assert getattr(graph, DEEPAGENTS_METADATA_KEY) is True invoke_config = graph.invoke("hello") - assert ( - invoke_config["metadata"][AGENT_FLAVOR_METADATA_KEY] - == DEEPAGENTS_AGENT_FLAVOR - ) assert invoke_config["metadata"][DEEPAGENTS_METADATA_KEY] is True stream_iter = graph.stream("hello") @@ -124,29 +114,19 @@ def test_uninstrument_restores_wrapped_graph_methods(): child_graph = graph.with_config({"metadata": {"customer": "kept"}}) assert getattr(child_graph, GRAPH_METHODS_WRAPPED_ATTR) is True - assert ( - getattr(child_graph, AGENT_FLAVOR_METADATA_KEY) - == DEEPAGENTS_AGENT_FLAVOR - ) assert getattr(child_graph, DEEPAGENTS_METADATA_KEY) is True child_config = child_graph.invoke("hello") assert child_config["metadata"]["customer"] == "kept" - assert ( - child_config["metadata"][AGENT_FLAVOR_METADATA_KEY] - == DEEPAGENTS_AGENT_FLAVOR - ) assert child_config["metadata"][DEEPAGENTS_METADATA_KEY] is True uninstrument_create_deep_agent() assert not hasattr(graph, GRAPH_METHODS_WRAPPED_ATTR) assert not hasattr(graph, GRAPH_ORIGINAL_METHODS_ATTR) - assert not hasattr(graph, AGENT_FLAVOR_METADATA_KEY) assert not hasattr(graph, REACT_AGENT_METADATA_KEY) assert not hasattr(graph, DEEPAGENTS_METADATA_KEY) assert not hasattr(child_graph, GRAPH_METHODS_WRAPPED_ATTR) assert not hasattr(child_graph, GRAPH_ORIGINAL_METHODS_ATTR) - assert not hasattr(child_graph, AGENT_FLAVOR_METADATA_KEY) assert not hasattr(child_graph, REACT_AGENT_METADATA_KEY) assert not hasattr(child_graph, DEEPAGENTS_METADATA_KEY) assert graph.invoke("hello") is None diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md index 109e831db..11561c0da 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md index 6cdfa9f61..28c8afe35 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/README.md @@ -2,67 +2,298 @@ LoongSuite instrumentation for [DeerFlow](https://github.com/bytedance/deer-flow) 2.x. -## Installation +## Features -Install DeerFlow from its official Git repository, then install this package: +- 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 -pip install 'deerflow-harness @ git+https://github.com/bytedance/deer-flow.git@v2.0.0#subdirectory=backend/packages/harness' -pip install loongsuite-instrumentation-deerflow +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 ``` -The similarly named `deerflow-harness` package currently published on PyPI is -not the official DeerFlow 2.x distribution. This instrumentation deliberately -does not expose an `instruments` extra that would install it. +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 -## Usage +# 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() ``` -The instrumentor composes the LoongSuite LangChain and LangGraph -instrumentations, identifies DeerFlow-created graphs, and creates an `ENTRY` -span around Gateway runs and embedded `DeerFlowClient.stream()` calls. Direct -SDK graph invocation starts at the graph's `AGENT` span and does not create a -synthetic application entry. +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. -The core execution tree is: +## Configuration -```text -ENTRY deerflow -└── AGENT lead-agent - └── STEP - ├── LLM - └── TOOL task - └── AGENT subagent: +### 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 ``` -An existing host `ENTRY` takes precedence over the Gateway and embedded entry -points. The embedded iterator restores its call-time context around every -`next()` and `close()`, including cross-thread consumption, without leaking -that context back to the consumer. +Authentication headers, TLS certificates, and signal-specific endpoints use +the standard OpenTelemetry exporter environment variables. -`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 expose their real `deerflow.run.id`. -Embedded runs omit that field because DeerFlow's public stream API does not -expose its internally generated run id. +### Content capture -Message content is disabled by default and follows the existing experimental -GenAI content-capture switch. +Message content is disabled by default. To capture it explicitly: -`task` remains a normal LangChain tool. Operations performed in a sandbox are -visible when DeerFlow exposes them as LangChain tools, and work selected by a -skill remains visible through its downstream LLM and tool calls. This package -does not create separate lifecycle spans for sandbox allocation or reuse, skill -activation, or background memory work, and it does not patch DeerFlow's context -propagation internals. +```bash +export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental +export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY +``` -## Compatibility +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. -- DeerFlow `>=2,<3`, installed from the official source repository -- Python 3.12+ +## 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/src/opentelemetry/instrumentation/deerflow/internal/constants.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/src/opentelemetry/instrumentation/deerflow/internal/constants.py index d0e67cadf..ba72cc2b8 100644 --- 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 @@ -14,8 +14,10 @@ """Constants shared by DeerFlow patch helpers.""" -AGENT_FLAVOR_ATTR = "_loongsuite_agent_flavor" -DEERFLOW_AGENT_FLAVOR = "deerflow" +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" 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 index b2465eb68..cde033304 100644 --- 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 @@ -39,10 +39,12 @@ from opentelemetry.util.genai.types import Error from .constants import ( - AGENT_FLAVOR_ATTR, + AGENT_FRAMEWORK_ATTR, + AGENT_STEP_NODE_ATTR, CLIENT_STREAM, CREATE_AGENT_ALIASES, - DEERFLOW_AGENT_FLAVOR, + DEERFLOW_AGENT_FRAMEWORK, + DEERFLOW_AGENT_STEP_NODE, DEERFLOW_RUN_STATUS, GATEWAY_LOADED_RUN_AGENT_ALIASES, GATEWAY_RUN_AGENT_ALIASES, @@ -101,13 +103,18 @@ def _track_graph(graph: Any, original_markers: dict[str, Any]) -> None: def mark_deerflow_graph(graph: Any) -> Any: - """Mark a graph with the DeerFlow flavor and no legacy ReAct flags.""" - marker_attrs = (AGENT_FLAVOR_ATTR, *_LEGACY_GRAPH_ATTRS) + """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_FLAVOR_ATTR, DEERFLOW_AGENT_FLAVOR) + 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) @@ -173,8 +180,11 @@ def _client_trace_id(instance: Any) -> str | None: getattr(instance, "_app_config", None) ): return generate_trace_id() - except (ImportError, ModuleNotFoundError): - pass + except Exception: # noqa: BLE001 + logger.debug( + "Failed to resolve DeerFlow trace correlation id", + exc_info=True, + ) return None @@ -187,7 +197,11 @@ def _bind_deerflow_trace_id(trace_id: str | None) -> Any: ) return set_current_trace_id(trace_id) - except (ImportError, ModuleNotFoundError): + except Exception: # noqa: BLE001 + logger.debug( + "Failed to bind DeerFlow trace correlation id", + exc_info=True, + ) return None @@ -200,7 +214,7 @@ def _reset_deerflow_trace_id(token: Any) -> None: ) reset_current_trace_id(token) - except (ImportError, ModuleNotFoundError, RuntimeError, ValueError): + except Exception: # noqa: BLE001 logger.debug("Failed to reset DeerFlow trace context", exc_info=True) @@ -338,8 +352,11 @@ async def __call__( return await wrapped(*args, **kwargs) record = _call_arg(args, kwargs, 2, "record") - graph_input = _call_arg(args, kwargs, 5, "graph_input") - config = _call_arg(args, kwargs, 6, "config", {}) + # 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) @@ -620,6 +637,8 @@ def _patch_location( 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) @@ -736,7 +755,13 @@ def _restore_late_gateway_aliases() -> None: def remove_owned_langchain_alias_wrappers() -> None: - """Remove cached LangChain wrappers after DeerFlow-owned teardown.""" + """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) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py index 54b889ca2..ad6254e3d 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_entry_wrappers.py @@ -18,6 +18,7 @@ import asyncio import json +import logging import sys import uuid from concurrent.futures import ThreadPoolExecutor @@ -590,6 +591,72 @@ def stream(_message, *, thread_id=None): 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, diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py index 4050c2508..0759dcbd7 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_instrumentor.py @@ -36,7 +36,8 @@ _deerflow_runtime_supported, ) from opentelemetry.instrumentation.deerflow.internal.constants import ( - AGENT_FLAVOR_ATTR, + AGENT_FRAMEWORK_ATTR, + AGENT_STEP_NODE_ATTR, GATEWAY_RUN_AGENT_ALIASES, ) from opentelemetry.instrumentation.deerflow.internal.patch import ( @@ -52,6 +53,17 @@ 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() == () @@ -255,7 +267,74 @@ def uninstrument_patches(): assert instrumentor._deerflow_patched is False -def test_create_agent_alias_marks_only_new_flavor(): +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( @@ -266,24 +345,29 @@ def test_create_agent_alias_marks_only_new_flavor(): ) assert result is graph - assert getattr(graph, AGENT_FLAVOR_ATTR) == "deerflow" + 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_FLAVOR_ATTR) + 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_flavor(): +def test_uninstrument_restores_previous_graph_semantics(): graph = _Graph() - setattr(graph, AGENT_FLAVOR_ATTR, "langchain-create-agent") + setattr(graph, AGENT_FRAMEWORK_ATTR, "custom-framework") + setattr(graph, AGENT_STEP_NODE_ATTR, "custom-step") mark_deerflow_graph(graph) - assert getattr(graph, AGENT_FLAVOR_ATTR) == "deerflow" + assert getattr(graph, AGENT_FRAMEWORK_ATTR) == "deerflow" + assert getattr(graph, AGENT_STEP_NODE_ATTR) == "model" uninstrument_deerflow() - assert getattr(graph, AGENT_FLAVOR_ATTR) == "langchain-create-agent" + 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( @@ -331,6 +415,28 @@ async def service_run_agent(*_args, **_kwargs): 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") diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py index 93a225f04..08feeafc1 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-deerflow/tests/test_runtime_graph.py @@ -294,7 +294,8 @@ def test_direct_sdk_sync_operations_have_agent_step_without_entry( graph.stream({"messages": [{"role": "user", "content": "hello"}]}) ) - assert getattr(graph, "_loongsuite_agent_flavor") == "deerflow" + 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, @@ -336,7 +337,8 @@ class _LangfuseRecorder( assert config["callbacks"] is callbacks assert config["metadata"] is metadata - assert "_loongsuite_agent_flavor" not in 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( diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/CHANGELOG.md index 75cd049f0..43e1dd1e6 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/CHANGELOG.md @@ -7,11 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -### Changed +### Added -- Distinguish LangGraph prebuilt, LangChain `create_agent`, DeepAgents, and - DeerFlow graphs with explicit agent flavors so each framework's model - decision node creates exactly one ReAct `STEP` span. +- 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) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/__init__.py index e825f3dcd..b67577c46 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/__init__.py @@ -40,11 +40,6 @@ from wrapt import wrap_function_wrapper from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( - AGENT_FLAVOR_METADATA_KEY, - LANGCHAIN_CREATE_AGENT_FLAVOR, - REACT_AGENT_METADATA_KEY, -) from opentelemetry.instrumentation.langchain.package import _instruments from opentelemetry.instrumentation.utils import unwrap @@ -145,19 +140,20 @@ def _uninstrument_agent_executor() -> None: # create_agent patch (langchain >= 1.x) # ------------------------------------------------------------------ +_REACT_AGENT_GRAPH_ATTR = "_loongsuite_react_agent" + def _create_agent_wrapper( wrapped: Any, _instance: Any, args: Any, kwargs: Any ) -> Any: """``wrapt`` wrapper for ``create_agent``. - Calls the original factory, then marks the returned graph as a LangChain - ``create_agent`` graph. The legacy boolean is retained for compatibility - with older LangGraph instrumentation. + Calls the original factory, then marks the returned graph with + ``_loongsuite_react_agent = True`` so that the langgraph Pregel + stream/astream wrapper can inject metadata into ``RunnableConfig``. """ graph = wrapped(*args, **kwargs) - setattr(graph, AGENT_FLAVOR_METADATA_KEY, LANGCHAIN_CREATE_AGENT_FLAVOR) - setattr(graph, REACT_AGENT_METADATA_KEY, True) + setattr(graph, _REACT_AGENT_GRAPH_ATTR, True) return graph diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_flavor.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_flavor.py deleted file mode 100644 index 5552cba49..000000000 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_agent_flavor.py +++ /dev/null @@ -1,60 +0,0 @@ -# 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 contract for distinguishing LangGraph-based agent harnesses.""" - -from __future__ import annotations - -from typing import Any - -AGENT_FLAVOR_METADATA_KEY = "_loongsuite_agent_flavor" -REACT_AGENT_METADATA_KEY = "_loongsuite_react_agent" -DEEPAGENTS_METADATA_KEY = "_loongsuite_deepagents_agent" - -LANGGRAPH_PREBUILT_AGENT_FLAVOR = "langgraph-prebuilt" -LANGCHAIN_CREATE_AGENT_FLAVOR = "langchain-create-agent" -DEEPAGENTS_AGENT_FLAVOR = "deepagents" -DEERFLOW_AGENT_FLAVOR = "deerflow" - -AGENT_FLAVOR_STEP_NODES = { - LANGGRAPH_PREBUILT_AGENT_FLAVOR: "agent", - LANGCHAIN_CREATE_AGENT_FLAVOR: "model", - DEEPAGENTS_AGENT_FLAVOR: "model", - DEERFLOW_AGENT_FLAVOR: "model", -} -SUPPORTED_AGENT_FLAVORS = frozenset(AGENT_FLAVOR_STEP_NODES) - - -def get_agent_flavor(run: Any) -> str | None: - """Return the single agent flavor carried by a callback run. - - The explicit flavor is authoritative. The boolean markers are read only as - compatibility fallbacks for graphs produced by older instrumentation. - """ - - metadata = getattr(run, "metadata", None) or {} - explicit_flavor = metadata.get(AGENT_FLAVOR_METADATA_KEY) - if explicit_flavor is not None: - return ( - explicit_flavor - if isinstance(explicit_flavor, str) - and explicit_flavor in SUPPORTED_AGENT_FLAVORS - else None - ) - - if metadata.get(DEEPAGENTS_METADATA_KEY): - return DEEPAGENTS_AGENT_FLAVOR - if metadata.get(REACT_AGENT_METADATA_KEY): - return LANGGRAPH_PREBUILT_AGENT_FLAVOR - return None 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 696b2606e..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 @@ -51,12 +51,13 @@ from opentelemetry import baggage from opentelemetry import context as otel_context from opentelemetry.context import Context -from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( - AGENT_FLAVOR_STEP_NODES, - DEERFLOW_AGENT_FLAVOR, - get_agent_flavor, +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, _documents_to_retrieval_documents, _extract_finish_reasons, _extract_invocation_params, @@ -68,6 +69,7 @@ _extract_token_usage, _extract_tool_definitions, _has_deepagents_metadata, + _has_langgraph_react_metadata, _is_agent_run, _safe_json, ) @@ -126,8 +128,14 @@ class _RunData: react_round: int = 0 active_step: "_RunData | None" = None original_context: Context | None = None - agent_flavor: str | None = None - inside_agent_flavor: str | None = None + is_langgraph_react: bool = False + 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 ) @@ -355,20 +363,42 @@ def _on_chain_start(self, run: Run) -> None: try: if _is_agent_run(run): self._start_agent(run) - elif get_agent_flavor(run) is not None: - self._handle_agent_chain_start(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): + self._handle_react_chain_start(run) else: self._start_chain(run) except Exception: logger.debug("Failed to start Chain/Agent span", exc_info=True) - def _handle_agent_chain_start(self, run: Run) -> None: - """Route a chain start that carries LoongSuite agent flavor metadata. + 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. Because ``config["metadata"]`` propagates to child callbacks, both the graph-level run and its child nodes carry the flag. We disambiguate by checking whether any ancestor is a marked - flavored agent or inside one: + ReAct-style agent or inside one: * **Inside marked agent** → child node (chain span, with possible ReAct step transition). @@ -379,19 +409,23 @@ def _handle_agent_chain_start(self, run: Run) -> None: parent_rd = self._runs.get(parent_id) if parent_id else None inside = parent_rd is not None and ( - parent_rd.agent_flavor is not None - or parent_rd.inside_agent_flavor is not None + parent_rd.is_langgraph_react + or parent_rd.inside_langgraph_react + or parent_rd.is_deepagents_react + or parent_rd.inside_deepagents_react ) if inside: - if parent_rd is not None and parent_rd.agent_flavor is not None: - self._maybe_enter_agent_step(run, parent_rd.agent_flavor) + if parent_rd is not None and parent_rd.is_deepagents_react: + self._maybe_enter_deepagents_react_step(run) + elif parent_rd is not None and parent_rd.is_langgraph_react: + self._maybe_enter_langgraph_react_step(run) self._start_chain(run) else: self._start_agent(run) def _resolve_agent_name(self, run: Run) -> str: - """Pick a meaningful name for a flavored LangGraph-based agent. + """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 @@ -399,8 +433,11 @@ def _resolve_agent_name(self, run: Run) -> str: over the generic default. """ name = run.name or "" - agent_flavor = get_agent_flavor(run) - if agent_flavor == DEERFLOW_AGENT_FLAVOR: + 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 @@ -424,7 +461,7 @@ def _resolve_agent_name(self, run: Run) -> str: return baggage_name.strip() return "lead-agent" - if agent_flavor is None or name != "LangGraph": + if not _has_langgraph_react_metadata(run) or name != "LangGraph": return name parent_id = getattr(run, "parent_run_id", None) @@ -473,16 +510,19 @@ def _start_agent(self, run: Run) -> None: agent_name=agent_name, input_messages=input_messages, ) - agent_flavor = get_agent_flavor(run) - if agent_flavor == DEERFLOW_AGENT_FLAVOR: - invocation.attributes["gen_ai.framework"] = "deerflow" + 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", span=invocation.span, context=otel_context.get_current() if invocation.span else None, invocation=invocation, - agent_flavor=agent_flavor, + 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 @@ -510,16 +550,28 @@ def _start_chain(self, run: Run) -> None: ctx = set_span_in_context(span, current_context) token = otel_context.attach(ctx) - # Propagate the agent flavor from parent so that + # Propagate framework-specific agent context from parent so that # grandchildren of the graph are also recognised as internal. - inside_agent_flavor = None + 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: with self._lock: p = self._runs.get(parent_id) if p is not None: - inside_agent_flavor = p.agent_flavor or p.inside_agent_flavor + inside_lg = p.is_langgraph_react or p.inside_langgraph_react + 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( @@ -527,7 +579,10 @@ def _start_chain(self, run: Run) -> None: span=span, context=ctx, context_token=token, - inside_agent_flavor=inside_agent_flavor, + 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: @@ -751,8 +806,8 @@ def _on_retriever_error(self, run: Run) -> None: # Agent STEP — callback-based detection # ------------------------------------------------------------------ - def _maybe_enter_agent_step(self, run: Run, agent_flavor: str) -> None: - """Start a STEP for a direct model decision node of a flavored agent. + 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. @@ -763,12 +818,11 @@ def _maybe_enter_agent_step(self, run: Run, agent_flavor: str) -> None: with self._lock: parent_rd = self._runs.get(parent_id) - if parent_rd is None or parent_rd.agent_flavor != agent_flavor: + if parent_rd is None or parent_rd.agent_framework is None: return - step_node = AGENT_FLAVOR_STEP_NODES.get(agent_flavor) chain_name = getattr(run, "name", "") or "" - if step_node is None or chain_name != step_node: + if chain_name != parent_rd.agent_step_node: return # End previous step (it had tool_calls since another round started) @@ -777,6 +831,46 @@ def _maybe_enter_agent_step(self, run: Run, agent_flavor: str) -> None: 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: + return + + chain_name = getattr(run, "name", "") or "" + if chain_name != LANGGRAPH_REACT_STEP_NODE: + return + + 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 a direct DeepAgents ``model`` 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_deepagents_react: + return + + chain_name = getattr(run, "name", "") or "" + if chain_name != DEEPAGENTS_REACT_STEP_NODE: + return + + if parent_rd.active_step is not None: + self._exit_react_step(parent_id, "tool_calls") + + self._enter_react_step(parent_id) + # ------------------------------------------------------------------ # ReAct Step — called from patch wrapper or callback detection # ------------------------------------------------------------------ diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py index be6f48c41..94f3530dc 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py @@ -18,11 +18,6 @@ import logging from typing import Any -from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( - DEEPAGENTS_AGENT_FLAVOR, - LANGGRAPH_PREBUILT_AGENT_FLAVOR, - get_agent_flavor, -) from opentelemetry.util.genai.extended_types import RetrievalDocument from opentelemetry.util.genai.types import ( FunctionToolDefinition, @@ -49,6 +44,9 @@ } ) +_LANGGRAPH_REACT_METADATA_KEY = "_loongsuite_react_agent" +_DEEPAGENTS_METADATA_KEY = "_loongsuite_deepagents_agent" + LANGGRAPH_REACT_STEP_NODE = "agent" DEEPAGENTS_REACT_STEP_NODE = "model" @@ -74,7 +72,8 @@ def _has_langgraph_react_metadata(run: Any) -> bool: Note: the metadata propagates to child runs, so the caller must distinguish the top-level graph from child nodes. """ - return get_agent_flavor(run) == LANGGRAPH_PREBUILT_AGENT_FLAVOR + metadata = getattr(run, "metadata", None) or {} + return bool(metadata.get(_LANGGRAPH_REACT_METADATA_KEY)) def _has_deepagents_metadata(run: Any) -> bool: @@ -85,7 +84,8 @@ def _has_deepagents_metadata(run: Any) -> bool: ``"model"`` instead of LangGraph ReAct's ``"agent"`` node, so the tracer uses this separate marker to preserve the framework-specific STEP rule. """ - return get_agent_flavor(run) == DEEPAGENTS_AGENT_FLAVOR + metadata = getattr(run, "metadata", None) or {} + return bool(metadata.get(_DEEPAGENTS_METADATA_KEY)) # --------------------------------------------------------------------------- diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_flavor.py b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_flavor.py deleted file mode 100644 index eebc736e0..000000000 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_flavor.py +++ /dev/null @@ -1,87 +0,0 @@ -# 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 - -from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( - AGENT_FLAVOR_METADATA_KEY, - AGENT_FLAVOR_STEP_NODES, - DEEPAGENTS_AGENT_FLAVOR, - DEEPAGENTS_METADATA_KEY, - DEERFLOW_AGENT_FLAVOR, - LANGCHAIN_CREATE_AGENT_FLAVOR, - LANGGRAPH_PREBUILT_AGENT_FLAVOR, - REACT_AGENT_METADATA_KEY, - get_agent_flavor, -) - - -def test_explicit_agent_flavor_overrides_legacy_markers(): - run = SimpleNamespace( - metadata={ - AGENT_FLAVOR_METADATA_KEY: LANGCHAIN_CREATE_AGENT_FLAVOR, - REACT_AGENT_METADATA_KEY: True, - DEEPAGENTS_METADATA_KEY: True, - } - ) - - assert get_agent_flavor(run) == LANGCHAIN_CREATE_AGENT_FLAVOR - - -def test_deepagents_marker_precedes_legacy_react_marker(): - run = SimpleNamespace( - metadata={ - REACT_AGENT_METADATA_KEY: True, - DEEPAGENTS_METADATA_KEY: True, - } - ) - - assert get_agent_flavor(run) == DEEPAGENTS_AGENT_FLAVOR - - -def test_legacy_react_marker_maps_to_langgraph_prebuilt(): - run = SimpleNamespace(metadata={REACT_AGENT_METADATA_KEY: True}) - - assert get_agent_flavor(run) == LANGGRAPH_PREBUILT_AGENT_FLAVOR - - -def test_unknown_explicit_agent_flavor_does_not_fall_back(): - run = SimpleNamespace( - metadata={ - AGENT_FLAVOR_METADATA_KEY: "unknown", - REACT_AGENT_METADATA_KEY: True, - } - ) - - assert get_agent_flavor(run) is None - - -def test_non_string_explicit_agent_flavor_does_not_fall_back(): - run = SimpleNamespace( - metadata={ - AGENT_FLAVOR_METADATA_KEY: ["deerflow"], - REACT_AGENT_METADATA_KEY: True, - } - ) - - assert get_agent_flavor(run) is None - - -def test_deerflow_explicit_flavor_uses_model_step_node(): - run = SimpleNamespace( - metadata={AGENT_FLAVOR_METADATA_KEY: DEERFLOW_AGENT_FLAVOR} - ) - - assert get_agent_flavor(run) == DEERFLOW_AGENT_FLAVOR - assert AGENT_FLAVOR_STEP_NODES[DEERFLOW_AGENT_FLAVOR] == "model" 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 5043ec6c1..0411231fb 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_spans.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_agent_spans.py @@ -19,9 +19,10 @@ import pytest from opentelemetry import baggage, context -from opentelemetry.instrumentation.langchain.internal._agent_flavor import ( - AGENT_FLAVOR_METADATA_KEY, - DEERFLOW_AGENT_FLAVOR, +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, @@ -171,7 +172,7 @@ def test_agent_context_colors_child_llm_and_tool_spans( assert tool_span.attributes[GenAI.GEN_AI_AGENT_NAME] == "AgentExecutor" -def test_deerflow_agent_flavor_sets_framework_attribute( +def test_deerflow_agent_semantics_sets_framework_attribute( tracer_provider, span_exporter ): handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider) @@ -180,7 +181,10 @@ def test_deerflow_agent_flavor_sets_framework_attribute( tracer_provider=tracer_provider, ) agent_run = _FakeRun("lead-agent", inputs={"input": "research"}) - agent_run.metadata = {AGENT_FLAVOR_METADATA_KEY: DEERFLOW_AGENT_FLAVOR} + 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"} @@ -232,7 +236,8 @@ def test_deerflow_agent_name_resolution( ) run = _FakeRun(name) run.metadata = { - AGENT_FLAVOR_METADATA_KEY: DEERFLOW_AGENT_FLAVOR, + AGENT_FRAMEWORK_METADATA_KEY: DEERFLOW_FRAMEWORK, + AGENT_STEP_NODE_METADATA_KEY: "model", **metadata, } run.tags = tags @@ -246,7 +251,10 @@ def test_deerflow_agent_name_falls_back_to_entry_baggage(tracer_provider): tracer_provider=tracer_provider, ) run = _FakeRun("LangGraph") - run.metadata = {AGENT_FLAVOR_METADATA_KEY: DEERFLOW_AGENT_FLAVOR} + 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) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/CHANGELOG.md index 134b2db9f..63135c869 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/CHANGELOG.md @@ -7,11 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -### Changed +### Added -- Propagate explicit agent flavor metadata through `Pregel.stream` and - `Pregel.astream` using copy-on-write config updates while retaining the - legacy ReAct marker fallback. +- 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) 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 7d11326eb..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 @@ -20,13 +20,13 @@ def wrapper(wrapped, instance, args, kwargs) -> ... Three patch targets: -1. ``create_react_agent`` — marks the compiled ``CompiledStateGraph`` with the - explicit ``langgraph-prebuilt`` agent flavor. +1. ``create_react_agent`` — sets ``_loongsuite_react_agent = True`` on the + compiled ``CompiledStateGraph`` so downstream instrumentation recognises it. 2. ``Pregel.stream`` / ``Pregel.astream`` — injects - ``metadata["_loongsuite_agent_flavor"]`` into a copy of the - ``RunnableConfig`` when the graph is a marked agent. This metadata flows - through LangChain's callback system to ``Run.metadata``. + 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 @@ -36,9 +36,9 @@ def wrapper(wrapped, instance, args, kwargs) -> ... logger = logging.getLogger(__name__) -AGENT_FLAVOR_METADATA_KEY = "_loongsuite_agent_flavor" +AGENT_FRAMEWORK_METADATA_KEY = "_loongsuite_agent_framework" +AGENT_STEP_NODE_METADATA_KEY = "_loongsuite_agent_step_node" REACT_AGENT_METADATA_KEY = "_loongsuite_react_agent" -LANGGRAPH_PREBUILT_AGENT_FLAVOR = "langgraph-prebuilt" # --------------------------------------------------------------------------- @@ -51,21 +51,16 @@ def _create_react_agent_wrapper( ) -> Any: """``wrapt`` wrapper for ``create_react_agent``. - Calls the original function, then marks the returned graph with its - explicit agent flavor. The legacy boolean remains for compatibility. + Calls the original function, then marks the returned graph with + ``_loongsuite_react_agent = True``. """ graph = wrapped(*args, **kwargs) - setattr( - graph, - AGENT_FLAVOR_METADATA_KEY, - LANGGRAPH_PREBUILT_AGENT_FLAVOR, - ) setattr(graph, REACT_AGENT_METADATA_KEY, True) logger.debug( "[INSTRUMENTATION] create_react_agent patched graph: name=%r, %s=%r", getattr(graph, "name", None), - AGENT_FLAVOR_METADATA_KEY, - LANGGRAPH_PREBUILT_AGENT_FLAVOR, + REACT_AGENT_METADATA_KEY, + True, ) return graph @@ -75,8 +70,10 @@ def _create_react_agent_wrapper( # --------------------------------------------------------------------------- -def _inject_agent_metadata(config: Any, agent_flavor: str) -> Any: - """Return a new config with the graph's single agent flavor 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 @@ -85,29 +82,45 @@ def _inject_agent_metadata(config: Any, agent_flavor: str) -> Any: config = ensure_config(config) config = {**config} - metadata = dict(config.get("metadata") or {}) - metadata[AGENT_FLAVOR_METADATA_KEY] = agent_flavor + 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 _get_graph_agent_flavor(graph: Any) -> str | None: - """Prefer an explicit graph flavor, falling back to the legacy marker.""" +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.""" - explicit_flavor = getattr(graph, AGENT_FLAVOR_METADATA_KEY, None) - if isinstance(explicit_flavor, str) and explicit_flavor: - return explicit_flavor - if getattr(graph, REACT_AGENT_METADATA_KEY, False): - return LANGGRAPH_PREBUILT_AGENT_FLAVOR - return None + 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``.""" - agent_flavor = _get_graph_agent_flavor(instance) - if agent_flavor is not None: - args, kwargs = _rewrite_config(args, kwargs, agent_flavor) + 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) @@ -115,9 +128,11 @@ async def _astream_wrapper( wrapped: Any, instance: Any, args: Any, kwargs: Any ): # type: ignore[return] """``wrapt`` wrapper for ``Pregel.astream``.""" - agent_flavor = _get_graph_agent_flavor(instance) - if agent_flavor is not None: - args, kwargs = _rewrite_config(args, kwargs, agent_flavor) + 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 @@ -125,13 +140,21 @@ async def _astream_wrapper( def _rewrite_config( args: tuple[Any, ...], kwargs: dict[str, Any], - agent_flavor: str, + 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_agent_metadata(args[1], agent_flavor) + 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_agent_metadata(kwargs.get("config"), agent_flavor) + 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 4e70e7ac7..6e633ac79 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-langgraph/tests/test_patch.py @@ -26,10 +26,12 @@ from opentelemetry.instrumentation.langgraph import LangGraphInstrumentor from opentelemetry.instrumentation.langgraph.internal.patch import ( - AGENT_FLAVOR_METADATA_KEY, - LANGGRAPH_PREBUILT_AGENT_FLAVOR, - _get_graph_agent_flavor, - _inject_agent_metadata, + AGENT_FRAMEWORK_METADATA_KEY, + AGENT_STEP_NODE_METADATA_KEY, + REACT_AGENT_METADATA_KEY, + _get_graph_agent_semantics, + _inject_agent_semantics, + _inject_react_metadata, ) # --------------------------------------------------------------------------- @@ -78,10 +80,6 @@ def test_patch_adds_react_flag(self, instrument): """The compiled graph has ``_loongsuite_react_agent = True``.""" graph = _build_react_agent() assert getattr(graph, "_loongsuite_react_agent", False) is True - assert ( - getattr(graph, AGENT_FLAVOR_METADATA_KEY) - == LANGGRAPH_PREBUILT_AGENT_FLAVOR - ) def test_patch_does_not_override_default_name(self, instrument): """The original graph name is preserved when no explicit name @@ -96,10 +94,6 @@ def test_patch_preserves_explicit_name(self, instrument): graph = _build_react_agent(name="my_custom_agent") assert graph.name == "my_custom_agent" assert getattr(graph, "_loongsuite_react_agent", False) is True - assert ( - getattr(graph, AGENT_FLAVOR_METADATA_KEY) - == LANGGRAPH_PREBUILT_AGENT_FLAVOR - ) def test_uninstrument_restores_original(self): """After ``uninstrument()``, ``create_react_agent`` returns an @@ -111,7 +105,6 @@ def test_uninstrument_restores_original(self): graph = _build_react_agent() assert not getattr(graph, "_loongsuite_react_agent", False) - assert not hasattr(graph, AGENT_FLAVOR_METADATA_KEY) def test_double_instrument_is_safe(self): """Calling ``instrument()`` twice does not break the wrapper.""" @@ -178,9 +171,7 @@ def test_metadata_injection_is_copy_on_write(self): "metadata": {"customer": "kept"}, } - rewritten = _inject_agent_metadata( - original, LANGGRAPH_PREBUILT_AGENT_FLAVOR - ) + rewritten = _inject_react_metadata(original) assert rewritten is not original assert rewritten["metadata"] is not original["metadata"] @@ -190,16 +181,36 @@ def test_metadata_injection_is_copy_on_write(self): } assert rewritten["callbacks"] == [callback] assert rewritten["metadata"]["customer"] == "kept" - assert ( - rewritten["metadata"][AGENT_FLAVOR_METADATA_KEY] - == LANGGRAPH_PREBUILT_AGENT_FLAVOR - ) + assert rewritten["metadata"][REACT_AGENT_METADATA_KEY] is True - def test_explicit_flavor_does_not_require_legacy_marker(self): + def test_opt_in_semantics_do_not_require_legacy_marker(self): graph = type("Graph", (), {})() - setattr(graph, AGENT_FLAVOR_METADATA_KEY, "deerflow") + 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"}, + } - assert _get_graph_agent_flavor(graph) == "deerflow" + 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.""" 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/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py index 39e84423c..31ba7075b 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agentscope.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-agentscope", - "instrumentation": "loongsuite-instrumentation-agentscope==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-agentscope==0.7.0.dev", "libraries": [ "agentscope >= 1.0.0, < 3.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py index 87bc4c5f9..8900567ca 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_agno.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-agno", - "instrumentation": "loongsuite-instrumentation-agno==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-agno==0.7.0.dev", "libraries": [ "agno >= 2.0.0, < 3", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py index 9c05ad93e..4d19cf800 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_algotune.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-algotune", - "instrumentation": "loongsuite-instrumentation-algotune==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-algotune==0.7.0.dev", "libraries": [], "default": True, } diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py index bdef66846..fad1a340b 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_autogen.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-autogen", - "instrumentation": "loongsuite-instrumentation-autogen==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-autogen==0.7.0.dev", "libraries": [ "autogen-agentchat >= 0.7.0, < 0.8.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py index d035dcd97..6413bfa83 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_bfclv4.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-bfclv4", - "instrumentation": "loongsuite-instrumentation-bfclv4==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-bfclv4==0.7.0.dev", "libraries": [ "bfcl-eval >= 4.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py index 10a019424..1e35bc2bc 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claude_agent_sdk.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-claude-agent-sdk", - "instrumentation": "loongsuite-instrumentation-claude-agent-sdk==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-claude-agent-sdk==0.7.0.dev", "libraries": [ "claude-agent-sdk >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py index d31178055..9fa624b93 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_claw_eval.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-claw-eval", - "instrumentation": "loongsuite-instrumentation-claw-eval==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-claw-eval==0.7.0.dev", "libraries": [ "claw-eval >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py index 107449a43..c0884657c 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_crewai.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-crewai", - "instrumentation": "loongsuite-instrumentation-crewai==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-crewai==0.7.0.dev", "libraries": [ "crewai >= 0.80.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py index 87bf8946b..bd4b7dd04 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dashscope.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-dashscope", - "instrumentation": "loongsuite-instrumentation-dashscope==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-dashscope==0.7.0.dev", "libraries": [ "dashscope >= 1.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py index aa9574de5..b22695004 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deepagents.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-deepagents", - "instrumentation": "loongsuite-instrumentation-deepagents==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-deepagents==0.7.0.dev", "libraries": [ "deepagents >= 0.6.0, < 0.7.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deerflow.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deerflow.py deleted file mode 100644 index 72a7461b1..000000000 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_deerflow.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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. - -# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. -# RUN `python scripts/loongsuite/generate_loongsuite_bootstrap.py` TO REGENERATE. -# -# Generated with options: -# --upstream-version: (from source) -# --loongsuite-version: (from source) - -REGISTRY = { - "source": "loongsuite", - "package": "loongsuite-instrumentation-deerflow", - "instrumentation": "loongsuite-instrumentation-deerflow==0.8.0.dev", - "libraries": [], - "default": True, -} diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py index e48cbea48..c62b18d1d 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_dify.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-dify", - "instrumentation": "loongsuite-instrumentation-dify==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-dify==0.7.0.dev", "libraries": [], "default": True, } diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py index 18c16a789..eae6d6912 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_google_adk.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-google-adk", - "instrumentation": "loongsuite-instrumentation-google-adk==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-google-adk==0.7.0.dev", "libraries": [ "google-adk >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py index 696112813..47406ffd8 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_hermes_agent.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-hermes-agent", - "instrumentation": "loongsuite-instrumentation-hermes-agent==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-hermes-agent==0.7.0.dev", "libraries": [ "openai >= 1.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py index 0f4b57f3c..d68d28959 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langchain.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-langchain", - "instrumentation": "loongsuite-instrumentation-langchain==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-langchain==0.7.0.dev", "libraries": [ "langchain_core >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py index be176f920..2b52ff124 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_langgraph.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-langgraph", - "instrumentation": "loongsuite-instrumentation-langgraph==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-langgraph==0.7.0.dev", "libraries": [ "langgraph >= 0.2", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py index cb9471392..eeca638e5 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_litellm.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-litellm", - "instrumentation": "loongsuite-instrumentation-litellm==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-litellm==0.7.0.dev", "libraries": [ "litellm >= 1.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py index 3c86370be..a293ee1db 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mcp.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-mcp", - "instrumentation": "loongsuite-instrumentation-mcp==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-mcp==0.7.0.dev", "libraries": [ "mcp >= 1.3.0, <= 1.25.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py index 90bf7b7b4..f20839dd3 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_mem0.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-mem0", - "instrumentation": "loongsuite-instrumentation-mem0==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-mem0==0.7.0.dev", "libraries": [ "mem0ai >= 1.0.0, < 2.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py index 2aeee9ef4..b6416e669 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_microsoft_agent_framework.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-microsoft-agent-framework", - "instrumentation": "loongsuite-instrumentation-microsoft-agent-framework==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-microsoft-agent-framework==0.7.0.dev", "libraries": [ "agent-framework-core >= 1.0.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py index 1945a10f9..10349e1d3 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_minisweagent.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-minisweagent", - "instrumentation": "loongsuite-instrumentation-minisweagent==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-minisweagent==0.7.0.dev", "libraries": [ "mini-swe-agent >= 2.2.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py index 20b60af35..adbaa570b 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_openhands.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-openhands", - "instrumentation": "loongsuite-instrumentation-openhands==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-openhands==0.7.0.dev", "libraries": [], "default": True, } diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py index 952e19150..cb087d592 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwen_agent.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-qwen-agent", - "instrumentation": "loongsuite-instrumentation-qwen-agent==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-qwen-agent==0.7.0.dev", "libraries": [ "qwen-agent >= 0.0.20", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py index d8aba3247..c244de352 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_qwenpaw.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-qwenpaw", - "instrumentation": "loongsuite-instrumentation-qwenpaw==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-qwenpaw==0.7.0.dev", "libraries": [ "qwenpaw >= 1.1.0", "copaw >= 0.1.0, <= 1.0.2", diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py index 2abb399fa..2084fe6fc 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_slop_code.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-slop-code", - "instrumentation": "loongsuite-instrumentation-slop-code==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-slop-code==0.7.0.dev", "libraries": [ "slop-code-bench >= 0.1", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py index 3711b6e59..09ce12c09 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_terminus2.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-terminus2", - "instrumentation": "loongsuite-instrumentation-terminus2==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-terminus2==0.7.0.dev", "libraries": [ "terminal-bench >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py index 62bf2376b..be49f87b2 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_vita.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-vita", - "instrumentation": "loongsuite-instrumentation-vita==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-vita==0.7.0.dev", "libraries": [ "vita >= 0.0.1", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py index dc4cf4a7a..dae9a5933 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_webarena.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-webarena", - "instrumentation": "loongsuite-instrumentation-webarena==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-webarena==0.7.0.dev", "libraries": [ "webarena >= 0.0.1", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py index bdc6f1e20..b1e846631 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_widesearch.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-widesearch", - "instrumentation": "loongsuite-instrumentation-widesearch==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-widesearch==0.7.0.dev", "libraries": [ "widesearch >= 0.1.0", ], diff --git a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py index ff5ace5ca..703fb6e2b 100644 --- a/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py +++ b/loongsuite-distro/src/loongsuite/distro/bootstrap_registry/loongsuite_instrumentation_wildtool.py @@ -22,7 +22,7 @@ REGISTRY = { "source": "loongsuite", "package": "loongsuite-instrumentation-wildtool", - "instrumentation": "loongsuite-instrumentation-wildtool==0.8.0.dev", + "instrumentation": "loongsuite-instrumentation-wildtool==0.7.0.dev", "libraries": [ "openai >= 1.0.0", ],