Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/loongsuite_lint_0.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-qwen-agent", "package": "loongsuite-instrumentation-qwen-agent", "tox_env": "lint-loongsuite-instrumentation-qwen-agent", "ui_name": "loongsuite-instrumentation-qwen-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-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: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/loongsuite_test_0.yml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed

- Create Hermes `ENTRY` spans for platform-less `AIAgent` calls by mirroring
Hermes session source resolution: `platform`, `HERMES_SESSION_SOURCE`, then
`cli`. This changes no-platform runs from `AGENT`-only to `ENTRY` -> `AGENT`
while leaving explicit CLI, IM, TUI, and API Server platform paths unchanged;
disable the Hermes instrumentation if a process must keep no-platform top
level agent calls as `AGENT`-only.

## Version 0.6.0 (2026-06-03)

There are no changelog entries for this release.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=NO_CONTENT
## Supported Signals

- **AGENT**: top-level Hermes agent invocation
- **ENTRY**: AI application entry spans when Hermes `AIAgent.platform` identifies an entrypoint such as CLI, TUI, API Server, or gateway adapters
- **ENTRY**: AI application entry spans when Hermes resolves an entry source
from `AIAgent.platform`, `HERMES_SESSION_SOURCE`, or the default `cli`
source used by platform-less `AIAgent` instances. Every top-level
instrumented `AIAgent.run_conversation` now emits `ENTRY` -> `AGENT`.
- **STEP**: Hermes ReAct step lifecycle
- **LLM**: synchronous and streaming model calls
- **TOOL**: Hermes tool execution, including tool call id, arguments, and result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import contextvars
import importlib
import json
import os
from types import SimpleNamespace
from typing import Any

Expand Down Expand Up @@ -59,6 +60,7 @@
)

_HERMES_AGENT_SYSTEM = "hermes"
_DEFAULT_ENTRY_PLATFORM = "cli"


def obj_get(value: Any, field: str, default: Any = None) -> Any:
Expand All @@ -72,9 +74,16 @@ def _normalize_platform(value: Any) -> str:
return str(platform or "").strip().lower()


def _entry_platform(instance: Any) -> str:
def resolve_entry_platform(instance: Any) -> str:
"""Resolve the Hermes entry source using AIAgent's session source order."""

platform = _normalize_platform(getattr(instance, "platform", None))
return platform
if platform:
return platform
platform = _normalize_platform(os.environ.get("HERMES_SESSION_SOURCE"))
if platform:
return platform
return _DEFAULT_ENTRY_PLATFORM


def to_int(value: Any) -> int:
Expand Down Expand Up @@ -605,10 +614,6 @@ def create_entry_invocation(
return invocation


def should_create_entry_for_agent(instance: Any) -> bool:
return bool(_entry_platform(instance))


def create_llm_invocation(instance: Any, api_kwargs: Any) -> LLMInvocation:
if not isinstance(api_kwargs, dict):
api_kwargs = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
provider_name,
push_state,
reset_state,
should_create_entry_for_agent,
resolve_entry_platform,
start_step,
state,
step_finish_reason,
Expand Down Expand Up @@ -148,8 +148,13 @@ def __call__(self, wrapped, instance, args, kwargs):
state_token = push_state(instance)
current_state = state(instance)
entry_invocation = None
# EntryInvocation has no source field yet; resolve the Hermes source
# here so entry creation follows Hermes's own session source default.
# TODO(loongsuite-hermes): pass entry_platform into EntryInvocation if
# opentelemetry-util-genai adds a session source field.
entry_platform = resolve_entry_platform(instance)
if (
should_create_entry_for_agent(instance)
entry_platform
and not _current_span_is_genai_operation()
and not _ACTIVE_TOOL_NAMES.get()
):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
from pathlib import Path

import pytest
from hermes_state import SessionDB

hermes_state = pytest.importorskip(
"hermes_state",
reason="Hermes runtime integration tests require the hermes-agent source.",
)
SessionDB = hermes_state.SessionDB


def _spans_by_kind(span_exporter, span_kind: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@

import pytest
from conftest import HermesAgentInstrumentor, extract_metric_points
from run_agent import AIAgent

run_agent = pytest.importorskip(
"run_agent",
reason="Hermes runtime integration tests require the hermes-agent source.",
)
AIAgent = run_agent.AIAgent


def _metric_value(point):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,12 +726,104 @@ def test_cli_platform_agent_creates_entry_parent_span(
_assert_parent(agent_span, entry_span)


def test_agent_without_platform_does_not_create_entry_span(
def test_tui_platform_agent_creates_entry_parent_span(
instrumentation_module,
tracer_provider,
meter_provider,
span_exporter,
):
runtime = _runtime(instrumentation_module, tracer_provider, meter_provider)
agent = _FakeAgent(session_id="tui-session", platform="tui")

runtime.run_wrapper(
lambda user_message: {"final_response": "完成"},
agent,
("请回复:完成",),
{},
)

entry_span = _spans_by_kind(span_exporter, "ENTRY")[0]
agent_span = _spans_by_kind(span_exporter, "AGENT")[0]
_assert_standard_entry_span(
entry_span,
session_id="tui-session",
input_text="请回复:完成",
output_text="完成",
)
_assert_parent(agent_span, entry_span)


def test_entry_platform_uses_env_session_source_when_agent_platform_missing(
monkeypatch,
):
helpers = importlib.import_module(
"opentelemetry.instrumentation.hermes_agent.helpers"
)
monkeypatch.setenv("HERMES_SESSION_SOURCE", " Web ")
agent = _FakeAgent(session_id="env-session")

assert helpers.resolve_entry_platform(agent) == "web"


def test_entry_platform_prefers_agent_platform_over_env_session_source(
monkeypatch,
):
helpers = importlib.import_module(
"opentelemetry.instrumentation.hermes_agent.helpers"
)
monkeypatch.setenv("HERMES_SESSION_SOURCE", "web")
agent = _FakeAgent(session_id="dingtalk-session", platform="dingtalk")

assert helpers.resolve_entry_platform(agent) == "dingtalk"


def test_entry_platform_empty_env_uses_cli_default(monkeypatch):
helpers = importlib.import_module(
"opentelemetry.instrumentation.hermes_agent.helpers"
)
monkeypatch.setenv("HERMES_SESSION_SOURCE", " ")
agent = _FakeAgent(session_id="default-session")

assert helpers.resolve_entry_platform(agent) == "cli"


def test_agent_without_platform_uses_env_session_source_for_entry(
instrumentation_module,
tracer_provider,
meter_provider,
span_exporter,
monkeypatch,
):
monkeypatch.setenv("HERMES_SESSION_SOURCE", "web")
runtime = _runtime(instrumentation_module, tracer_provider, meter_provider)
agent = _FakeAgent(session_id="env-entry-session")

runtime.run_wrapper(
lambda user_message: {"final_response": "完成"},
agent,
("请回复:完成",),
{},
)

entry_span = _spans_by_kind(span_exporter, "ENTRY")[0]
agent_span = _spans_by_kind(span_exporter, "AGENT")[0]
_assert_standard_entry_span(
entry_span,
session_id="env-entry-session",
input_text="请回复:完成",
output_text="完成",
)
_assert_parent(agent_span, entry_span)


def test_agent_without_platform_or_env_uses_cli_default_entry_source(
instrumentation_module,
tracer_provider,
meter_provider,
span_exporter,
monkeypatch,
):
monkeypatch.delenv("HERMES_SESSION_SOURCE", raising=False)
runtime = _runtime(instrumentation_module, tracer_provider, meter_provider)
agent = _FakeAgent(session_id="library-session")

Expand All @@ -742,8 +834,15 @@ def test_agent_without_platform_does_not_create_entry_span(
{},
)

assert _spans_by_kind(span_exporter, "ENTRY") == []
assert len(_spans_by_kind(span_exporter, "AGENT")) == 1
entry_span = _spans_by_kind(span_exporter, "ENTRY")[0]
agent_span = _spans_by_kind(span_exporter, "AGENT")[0]
_assert_standard_entry_span(
entry_span,
session_id="library-session",
input_text="请回复:完成",
output_text="完成",
)
_assert_parent(agent_span, entry_span)


def test_agent_span_does_not_backfill_agent_id_from_session_id(
Expand Down
Loading
Loading