Skip to content

Commit dc0d39f

Browse files
committed
fix(hermes-agent): create entry span from session source
1 parent 13f9339 commit dc0d39f

5 files changed

Lines changed: 133 additions & 12 deletions

File tree

instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Fixed
11+
12+
- Create Hermes `ENTRY` spans for platform-less `AIAgent` calls by mirroring
13+
Hermes session source resolution: `platform`, `HERMES_SESSION_SOURCE`, then
14+
`cli`. This changes no-platform runs from `AGENT`-only to `ENTRY` -> `AGENT`
15+
while leaving explicit CLI, IM, TUI, and API Server platform paths unchanged;
16+
disable the Hermes instrumentation if a process must keep no-platform top
17+
level agent calls as `AGENT`-only.
18+
1019
## Version 0.6.0 (2026-06-03)
1120

1221
There are no changelog entries for this release.

instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=NO_CONTENT
8080
## Supported Signals
8181

8282
- **AGENT**: top-level Hermes agent invocation
83-
- **ENTRY**: AI application entry spans when Hermes `AIAgent.platform` identifies an entrypoint such as CLI, TUI, API Server, or gateway adapters
83+
- **ENTRY**: AI application entry spans when Hermes resolves an entry source
84+
from `AIAgent.platform`, `HERMES_SESSION_SOURCE`, or the default `cli`
85+
source used by platform-less `AIAgent` instances. Every top-level
86+
instrumented `AIAgent.run_conversation` now emits `ENTRY` -> `AGENT`.
8487
- **STEP**: Hermes ReAct step lifecycle
8588
- **LLM**: synchronous and streaming model calls
8689
- **TOOL**: Hermes tool execution, including tool call id, arguments, and result

instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/helpers.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import contextvars
2020
import importlib
2121
import json
22+
import os
2223
from types import SimpleNamespace
2324
from typing import Any
2425

@@ -59,6 +60,7 @@
5960
)
6061

6162
_HERMES_AGENT_SYSTEM = "hermes"
63+
_DEFAULT_ENTRY_PLATFORM = "cli"
6264

6365

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

7476

75-
def _entry_platform(instance: Any) -> str:
77+
def resolve_entry_platform(instance: Any) -> str:
78+
"""Resolve the Hermes entry source using AIAgent's session source order."""
79+
7680
platform = _normalize_platform(getattr(instance, "platform", None))
77-
return platform
81+
if platform:
82+
return platform
83+
platform = _normalize_platform(os.environ.get("HERMES_SESSION_SOURCE"))
84+
if platform:
85+
return platform
86+
return _DEFAULT_ENTRY_PLATFORM
7887

7988

8089
def to_int(value: Any) -> int:
@@ -605,10 +614,6 @@ def create_entry_invocation(
605614
return invocation
606615

607616

608-
def should_create_entry_for_agent(instance: Any) -> bool:
609-
return bool(_entry_platform(instance))
610-
611-
612617
def create_llm_invocation(instance: Any, api_kwargs: Any) -> LLMInvocation:
613618
if not isinstance(api_kwargs, dict):
614619
api_kwargs = {}

instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/src/opentelemetry/instrumentation/hermes_agent/wrappers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
provider_name,
3737
push_state,
3838
reset_state,
39-
should_create_entry_for_agent,
39+
resolve_entry_platform,
4040
start_step,
4141
state,
4242
step_finish_reason,
@@ -148,8 +148,13 @@ def __call__(self, wrapped, instance, args, kwargs):
148148
state_token = push_state(instance)
149149
current_state = state(instance)
150150
entry_invocation = None
151+
# EntryInvocation has no source field yet; resolve the Hermes source
152+
# here so entry creation follows Hermes's own session source default.
153+
# TODO(loongsuite-hermes): pass entry_platform into EntryInvocation if
154+
# opentelemetry-util-genai adds a session source field.
155+
entry_platform = resolve_entry_platform(instance)
151156
if (
152-
should_create_entry_for_agent(instance)
157+
entry_platform
153158
and not _current_span_is_genai_operation()
154159
and not _ACTIVE_TOOL_NAMES.get()
155160
):

instrumentation-loongsuite/loongsuite-instrumentation-hermes-agent/tests/test_telemetry_spec.py

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -726,12 +726,104 @@ def test_cli_platform_agent_creates_entry_parent_span(
726726
_assert_parent(agent_span, entry_span)
727727

728728

729-
def test_agent_without_platform_does_not_create_entry_span(
729+
def test_tui_platform_agent_creates_entry_parent_span(
730730
instrumentation_module,
731731
tracer_provider,
732732
meter_provider,
733733
span_exporter,
734734
):
735+
runtime = _runtime(instrumentation_module, tracer_provider, meter_provider)
736+
agent = _FakeAgent(session_id="tui-session", platform="tui")
737+
738+
runtime.run_wrapper(
739+
lambda user_message: {"final_response": "完成"},
740+
agent,
741+
("请回复:完成",),
742+
{},
743+
)
744+
745+
entry_span = _spans_by_kind(span_exporter, "ENTRY")[0]
746+
agent_span = _spans_by_kind(span_exporter, "AGENT")[0]
747+
_assert_standard_entry_span(
748+
entry_span,
749+
session_id="tui-session",
750+
input_text="请回复:完成",
751+
output_text="完成",
752+
)
753+
_assert_parent(agent_span, entry_span)
754+
755+
756+
def test_entry_platform_uses_env_session_source_when_agent_platform_missing(
757+
monkeypatch,
758+
):
759+
helpers = importlib.import_module(
760+
"opentelemetry.instrumentation.hermes_agent.helpers"
761+
)
762+
monkeypatch.setenv("HERMES_SESSION_SOURCE", " Web ")
763+
agent = _FakeAgent(session_id="env-session")
764+
765+
assert helpers.resolve_entry_platform(agent) == "web"
766+
767+
768+
def test_entry_platform_prefers_agent_platform_over_env_session_source(
769+
monkeypatch,
770+
):
771+
helpers = importlib.import_module(
772+
"opentelemetry.instrumentation.hermes_agent.helpers"
773+
)
774+
monkeypatch.setenv("HERMES_SESSION_SOURCE", "web")
775+
agent = _FakeAgent(session_id="dingtalk-session", platform="dingtalk")
776+
777+
assert helpers.resolve_entry_platform(agent) == "dingtalk"
778+
779+
780+
def test_entry_platform_empty_env_uses_cli_default(monkeypatch):
781+
helpers = importlib.import_module(
782+
"opentelemetry.instrumentation.hermes_agent.helpers"
783+
)
784+
monkeypatch.setenv("HERMES_SESSION_SOURCE", " ")
785+
agent = _FakeAgent(session_id="default-session")
786+
787+
assert helpers.resolve_entry_platform(agent) == "cli"
788+
789+
790+
def test_agent_without_platform_uses_env_session_source_for_entry(
791+
instrumentation_module,
792+
tracer_provider,
793+
meter_provider,
794+
span_exporter,
795+
monkeypatch,
796+
):
797+
monkeypatch.setenv("HERMES_SESSION_SOURCE", "web")
798+
runtime = _runtime(instrumentation_module, tracer_provider, meter_provider)
799+
agent = _FakeAgent(session_id="env-entry-session")
800+
801+
runtime.run_wrapper(
802+
lambda user_message: {"final_response": "完成"},
803+
agent,
804+
("请回复:完成",),
805+
{},
806+
)
807+
808+
entry_span = _spans_by_kind(span_exporter, "ENTRY")[0]
809+
agent_span = _spans_by_kind(span_exporter, "AGENT")[0]
810+
_assert_standard_entry_span(
811+
entry_span,
812+
session_id="env-entry-session",
813+
input_text="请回复:完成",
814+
output_text="完成",
815+
)
816+
_assert_parent(agent_span, entry_span)
817+
818+
819+
def test_agent_without_platform_or_env_uses_cli_default_entry_source(
820+
instrumentation_module,
821+
tracer_provider,
822+
meter_provider,
823+
span_exporter,
824+
monkeypatch,
825+
):
826+
monkeypatch.delenv("HERMES_SESSION_SOURCE", raising=False)
735827
runtime = _runtime(instrumentation_module, tracer_provider, meter_provider)
736828
agent = _FakeAgent(session_id="library-session")
737829

@@ -742,8 +834,15 @@ def test_agent_without_platform_does_not_create_entry_span(
742834
{},
743835
)
744836

745-
assert _spans_by_kind(span_exporter, "ENTRY") == []
746-
assert len(_spans_by_kind(span_exporter, "AGENT")) == 1
837+
entry_span = _spans_by_kind(span_exporter, "ENTRY")[0]
838+
agent_span = _spans_by_kind(span_exporter, "AGENT")[0]
839+
_assert_standard_entry_span(
840+
entry_span,
841+
session_id="library-session",
842+
input_text="请回复:完成",
843+
output_text="完成",
844+
)
845+
_assert_parent(agent_span, entry_span)
747846

748847

749848
def test_agent_span_does_not_backfill_agent_id_from_session_id(

0 commit comments

Comments
 (0)