diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 248228a..3a20db4 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -27,6 +27,7 @@ import { TOOL_NAME, TOOL_PARAMETERS, } from "@arizeai/openinference-semantic-conventions" +import { ATTR_GEN_AI_AGENT_NAME } from "@opentelemetry/semantic-conventions/incubating" import { agentAttrs, errorSummary, @@ -123,6 +124,7 @@ export function handleMessageUpdated(e: EventMessageUpdated, ctx: HandlerContext const outputText = ctx.messageOutputs.get(msgKey) msgSpan.setAttributes({ [AGENT_NAME]: agentName, + [ATTR_GEN_AI_AGENT_NAME]: agentName, "agent.type": agentType, [LLM_TOKEN_COUNT_PROMPT]: assistant.tokens.input, [LLM_TOKEN_COUNT_COMPLETION]: assistant.tokens.output, @@ -449,6 +451,7 @@ export function startMessageSpan( [OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.LLM, [SESSION_ID]: sessionID, [AGENT_NAME]: agentName, + [ATTR_GEN_AI_AGENT_NAME]: agentName, "agent.type": agentType, [LLM_SYSTEM]: providerID, [LLM_PROVIDER]: providerID, diff --git a/tests/handlers/spans.test.ts b/tests/handlers/spans.test.ts index 18851d0..d3d0106 100644 --- a/tests/handlers/spans.test.ts +++ b/tests/handlers/spans.test.ts @@ -370,6 +370,15 @@ describe("message (LLM) spans", () => { expect(tracer.spans[0]!.attributes[LLM_MODEL_NAME]).toBe("claude-sonnet-4") }) + // Asserted as a literal key, not via the semconv constant the source imports: + // this string is the wire format Gen AI consumers match on, so the test has to + // fail if the constant is renamed upstream. + test("startMessageSpan mirrors the agent name onto gen_ai.agent.name", () => { + const { ctx, tracer } = makeCtx() + startMessageSpan("ses_1", "msg_1", "user_1", "claude-sonnet-4", "anthropic", 1000, ctx, "plan") + expect(tracer.spans[0]!.attributes["gen_ai.agent.name"]).toBe(tracer.spans[0]!.attributes[AGENT_NAME]) + }) + test("startMessageSpan is a no-op when span already exists for sessionID:messageID", () => { const { ctx, tracer } = makeCtx() startMessageSpan("ses_1", "msg_1", "user_1", "claude", "anthropic", 1000, ctx) @@ -434,6 +443,17 @@ describe("message (LLM) spans", () => { expect(span.attributes["agent.type"]).toBe("subagent") }) + test("handleMessageUpdated mirrors the agent name onto gen_ai.agent.name", () => { + const { ctx, tracer } = makeCtx() + startMessageSpan("ses_1", "msg_1", "user_1", "claude-3-5-sonnet", "anthropic", 1000, ctx) + ctx.sessionTotals.set("ses_1", { startMs: 0, tokens: 0, cost: 0, messages: 0, agent: "review", agentType: "subagent" }) + handleMessageUpdated(makeAssistantMessageUpdated({ id: "msg_1" }), ctx) + const span = tracer.spans[0]! + expect(span.attributes["gen_ai.agent.name"]).toBe("review") + // the OpenInference key is mirrored, never replaced + expect(span.attributes[AGENT_NAME]).toBe("review") + }) + test("handleMessageUpdated no-ops span handling when no span exists for messageID", () => { const { ctx, tracer } = makeCtx() const spansBefore = tracer.spans.length