Skip to content
6 changes: 6 additions & 0 deletions docs/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,7 @@ llmobs.annotate(span, {

// declare an agent
const agentDeclaration: import('..').llmobs.Agent = {
version: '2.1.0',
name: 'travel_desk',
instructions: 'Book travel.',
model: 'gpt-4o',
Expand All @@ -898,6 +899,11 @@ const agentDeclaration: import('..').llmobs.Agent = {
}
llmobs.annotate({ agent: agentDeclaration })
llmobs.annotationContext({ agent: agentDeclaration }, () => {})
llmobs.annotate({ agent: { version: '2.1.1' } })
llmobs.trace({ kind: 'agent', name: 'travel_desk', version: '2.1.0' }, () => {})
llmobs.wrap({ kind: 'agent', version: '2.1.0' }, () => {})
// @ts-expect-error An agent version must be a string, so it is reported exactly as written.
llmobs.wrap({ kind: 'agent', version: 2 }, () => {})



Expand Down
20 changes: 15 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4867,11 +4867,14 @@ declare namespace tracer {
}

/**
* Declares the agent an `agent` span represents, reported as the agent's manifest. Only applies to
* `agent` spans. Unreportable values are dropped with a warning, and unset values (`undefined`, `null`,
* `''`, `[]`) leave what an earlier annotation declared in place.
* Declares the agent an `agent` span represents. `version` is set as an `agent_version` tag on the agent
* span and its child spans within the same process. The other fields are reported as the agent's manifest.
* Only applies to `agent` spans. Unreportable values are dropped with a warning, and unset values
* (`undefined`, `null`, `''`, `[]`) leave what an earlier annotation declared in place.
*/
interface Agent {
/** The version of the agent. */
version?: string,
/** The agent's name. Defaults to the agent span's name. */
name?: string,
/** The system instructions the agent runs with. */
Expand Down Expand Up @@ -4966,8 +4969,8 @@ declare namespace tracer {
prompt?: Prompt,

/**
* Declares the agent running in this context, read when the context is entered. The manifest is
* reported on every `agent` span in the context, nested ones included. Use `annotate` on a nested agent
* Declares the agent running in this context, read when the context is entered. The version and manifest
* are reported on every `agent` span in the context, nested ones included. Use `annotate` on a nested agent
* span to declare its own agent.
*/
agent?: Agent,
Expand Down Expand Up @@ -5028,6 +5031,13 @@ declare namespace tracer {
* If not provided for LLM or embedding spans, a default value of 'custom' will be set.
*/
modelProvider?: string,

/**
* The version of the agent, set as an `agent_version` tag on the agent span and its child spans within the
* same process. Only used on `agent` spans, and wins over a version declared by an enclosing
* `annotationContext`.
*/
version?: string,
}

interface LLMObsNamedSpanOptions extends LLMObsSpanOptions {
Expand Down
20 changes: 15 additions & 5 deletions index.d.v5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5065,11 +5065,14 @@ declare namespace tracer {
}

/**
* Declares the agent an `agent` span represents, reported as the agent's manifest. Only applies to
* `agent` spans. Unreportable values are dropped with a warning, and unset values (`undefined`, `null`,
* `''`, `[]`) leave what an earlier annotation declared in place.
* Declares the agent an `agent` span represents. `version` is set as an `agent_version` tag on the agent
* span and its child spans within the same process. The other fields are reported as the agent's manifest.
* Only applies to `agent` spans. Unreportable values are dropped with a warning, and unset values
* (`undefined`, `null`, `''`, `[]`) leave what an earlier annotation declared in place.
*/
interface Agent {
/** The version of the agent. */
version?: string,
/** The agent's name. Defaults to the agent span's name. */
name?: string,
/** The system instructions the agent runs with. */
Expand Down Expand Up @@ -5165,8 +5168,8 @@ declare namespace tracer {
prompt?: Prompt,

/**
* Declares the agent running in this context, read when the context is entered. The manifest is
* reported on every `agent` span in the context, nested ones included. Use `annotate` on a nested agent
* Declares the agent running in this context, read when the context is entered. The version and manifest
* are reported on every `agent` span in the context, nested ones included. Use `annotate` on a nested agent
* span to declare its own agent.
*/
agent?: Agent,
Expand Down Expand Up @@ -5227,6 +5230,13 @@ declare namespace tracer {
* If not provided for LLM or embedding spans, a default value of 'custom' will be set.
*/
modelProvider?: string,

/**
* The version of the agent, set as an `agent_version` tag on the agent span and its child spans within the
* same process. Only used on `agent` spans, and wins over a version declared by an enclosing
* `annotationContext`.
*/
version?: string,
}

interface LLMObsNamedSpanOptions extends LLMObsSpanOptions {
Expand Down
16 changes: 14 additions & 2 deletions packages/dd-trace/src/llmobs/agent-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const LABEL_FIELDS = ['name', 'instructions', 'model']
* model_settings?: Record<string, unknown>,
* tools?: AgentTool[],
* }} AgentManifestFields
* @typedef {{ manifest: AgentManifestFields }} AgentDeclaration
* @typedef {{ version?: string, manifest?: AgentManifestFields }} AgentDeclaration
*/

/**
Expand All @@ -50,8 +50,9 @@ function buildAgentDeclaration (agent) {
let declaration
try {
if (isPlainObject(agent)) {
const version = toAgentVersion(agent.version)
const manifest = buildAgentManifest(agent)
if (manifest !== undefined) declaration = { manifest }
if (version !== undefined || manifest !== undefined) declaration = { version, manifest }
} else {
log.warn('Dropping agent annotation, the agent must be a plain object.')
}
Expand Down Expand Up @@ -105,6 +106,16 @@ function buildAgentManifest (agent) {
return manifest
}

/**
* Unset values (`undefined`, `null`, `''`) are ignored without a warning, so they leave an earlier version in place.
*
* @param {unknown} version
*/
function toAgentVersion (version) {
if (typeof version === 'string' && version !== '') return version
if (!isUnset(version)) log.warn('Dropping the agent version, it must be a string.')
}

/**
* @param {unknown} settings
* @param {string[]} dropped
Expand Down Expand Up @@ -293,4 +304,5 @@ function isScalar (value) {
module.exports = {
MANUAL_FRAMEWORK_NAME,
buildAgentDeclaration,
toAgentVersion,
}
3 changes: 3 additions & 0 deletions packages/dd-trace/src/llmobs/constants/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ module.exports = {
METADATA: '_ml_obs.meta.metadata',
COST_TAGS: '_ml_obs.meta.metadata._dd.cost_tags',
AGENT_MANIFEST: '_ml_obs.meta.metadata._dd.agent_manifest',
AGENT_VERSION: '_ml_obs.agent_version',
AGENT_VERSION_TAG_KEY: 'agent_version',
METRICS: '_ml_obs.metrics',
TOOL_DEFINITIONS: '_ml_obs.meta.tool_definitions',
ML_APP: '_ml_obs.meta.ml_app',
Expand All @@ -29,6 +31,7 @@ module.exports = {

PARENT_AGENT_NAME: '_ml_obs.parent_agent_name',
PARENT_AGENT_SPAN_ID: '_ml_obs.parent_agent_span_id',
PARENT_AGENT_VERSION: '_ml_obs.parent_agent_version',
PROPAGATED_PARENT_AGENT_ID_KEY: '_dd.p.llmobs_pagent_span_id',
PROPAGATED_PARENT_AGENT_NAME_KEY: '_dd.p.llmobs_pagent_name',
PROPAGATED_SAMPLE_RATE_KEY: '_dd.p.llmobs_sr',
Expand Down
19 changes: 15 additions & 4 deletions packages/dd-trace/src/llmobs/sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const {
getFunctionArguments,
validateKind,
} = require('./util')
const { buildAgentDeclaration } = require('./agent-manifest')
const { buildAgentDeclaration, toAgentVersion } = require('./agent-manifest')
/** @typedef {import('./agent-manifest').AgentDeclaration} AgentDeclaration */
const { storage } = require('./storage')
const telemetry = require('./telemetry')
Expand Down Expand Up @@ -163,7 +163,7 @@ class LLMObs extends NoopLLMObs {
const {
spanOptions,
...llmobsOptions
} = this.#extractOptions(options)
} = this.#extractOptions(options, kind)

if (fn.length > 1) {
return this._tracer.trace(name, spanOptions, (span, cb) =>
Expand Down Expand Up @@ -193,7 +193,7 @@ class LLMObs extends NoopLLMObs {
const {
spanOptions,
...llmobsOptions
} = this.#extractOptions(options)
} = this.#extractOptions(options, kind)

const llmobs = this

Expand Down Expand Up @@ -719,21 +719,32 @@ class LLMObs extends NoopLLMObs {
}
}

#extractOptions (options) {
#extractOptions (options, kind) {
const {
modelName,
modelProvider,
sessionId,
mlApp,
version,
_decorator,
...spanOptions
} = options

let agentVersion
if (version !== undefined) {
if (kind === 'agent') {
agentVersion = toAgentVersion(version)
} else {
logger.warn('Dropping the version option on a %s span, it is only supported for agent spans.', kind)
}
}

return {
mlApp,
modelName,
modelProvider,
sessionId,
agentVersion,
_decorator,
spanOptions,
}
Expand Down
12 changes: 12 additions & 0 deletions packages/dd-trace/src/llmobs/span_processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const {
METADATA,
COST_TAGS,
AGENT_MANIFEST,
AGENT_VERSION,
AGENT_VERSION_TAG_KEY,
TOOL_DEFINITIONS,
EXPERIMENT_INPUT,
EXPERIMENT_OUTPUT,
Expand All @@ -32,6 +34,7 @@ const {
PARENT_ID_KEY,
PARENT_AGENT_NAME,
PARENT_AGENT_SPAN_ID,
PARENT_AGENT_VERSION,
SESSION_ID,
NAME,
INPUT_PROMPT,
Expand Down Expand Up @@ -407,6 +410,15 @@ class LLMObsSpanProcessor {
const existingTags = LLMObsTagger.tagMap.get(span)?.[TAGS] || {}
if (existingTags) tags = { ...tags, ...existingTags }

const mlObsTags = LLMObsTagger.tagMap.get(span)
// Resolved here because a span can become an agent after registration. An agent without its own version, and
// any other span, reports the version inherited at registration. Either wins over a user
// tag of the same name.
const agentVersion = mlObsTags?.[SPAN_KIND] === 'agent'
? mlObsTags[AGENT_VERSION] || mlObsTags[PARENT_AGENT_VERSION]
: mlObsTags?.[PARENT_AGENT_VERSION]
if (agentVersion) tags[AGENT_VERSION_TAG_KEY] = agentVersion

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject versions that cannot survive tag serialization

When an agent version contains a comma (for example, release,canary), this copies it verbatim into the tag map, and #objectTagsToStringArrayTags later emits agent_version:release,canary. That serializer explicitly documents commas as intake-side delimiters, so the value is split into agent_version:release plus an orphan tag instead of being reported as the supplied version. Validate or safely encode agent versions before adding this tag.

Useful? React with 👍 / 👎.


return tags
}

Expand Down
26 changes: 18 additions & 8 deletions packages/dd-trace/src/llmobs/tagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
METADATA,
COST_TAGS,
AGENT_MANIFEST,
AGENT_VERSION,
METRICS,
TOOL_DEFINITIONS,
PARENT_ID_KEY,
Expand All @@ -28,6 +29,7 @@ const {
NAME,
PARENT_AGENT_NAME,
PARENT_AGENT_SPAN_ID,
PARENT_AGENT_VERSION,
PROPAGATED_PARENT_ID_KEY,
PROPAGATED_PARENT_AGENT_ID_KEY,
PROPAGATED_PARENT_AGENT_NAME_KEY,
Expand Down Expand Up @@ -116,6 +118,7 @@ class LLMObsTagger {
kind,
name,
integration,
agentVersion,
_decorator,
} = {}) {
if (!this.#config.llmobs.DD_LLMOBS_ENABLED) return
Expand Down Expand Up @@ -193,6 +196,8 @@ class LLMObsTagger {
storage.getStore()?.agentDeclarations
)
if (agentDeclarations) this.#applyAgentDeclarations(span, agentDeclarations)
// The span's own version wins over the enclosing contexts.
if (agentVersion) this._setTag(span, AGENT_VERSION, agentVersion)

// apply annotation context name
const annotationContextName = annotationContext?.name
Expand All @@ -213,15 +218,16 @@ class LLMObsTagger {

/**
* Applies the agents declared by the enclosing annotation contexts, outermost first, to every span in the block.
* The manifest is emitted only if the span is an agent when it finishes, since some integrations promote a span
* to an agent after registration.
* Both are emitted only if the span is an agent when it finishes, since some integrations promote a span to an
* agent after registration.
*
* @param {import('../opentracing/span')} span
* @param {import('./agent-manifest').AgentDeclaration[]} declarations
*/
#applyAgentDeclarations (span, declarations) {
for (const declaration of declarations) {
this.#tagAgentManifestFields(span, declaration.manifest)
if (declaration.version) this._setTag(span, AGENT_VERSION, declaration.version)
if (declaration.manifest) this.#tagAgentManifestFields(span, declaration.manifest)
}
}

Expand All @@ -233,7 +239,9 @@ class LLMObsTagger {
*/
tagAgent (span, agent) {
const declaration = buildAgentDeclaration(agent)
if (declaration) this.#tagAgentManifestFields(span, declaration.manifest)
if (!declaration) return
if (declaration.version) this._setTag(span, AGENT_VERSION, declaration.version)
if (declaration.manifest) this.#tagAgentManifestFields(span, declaration.manifest)
}

/**
Expand Down Expand Up @@ -276,19 +284,20 @@ class LLMObsTagger {

/**
* Store the nearest agent ancestor on the span so it can be surfaced as
* `meta.agent_attribution` at finish. Resolved once here, at registration, so downstream
* children inherit it with a single lookup rather than walking the ancestor chain.
* `meta.agent_attribution` at finish, along with that agent's version, which the agent panel reads
* off each span to attribute its spend. Resolved once here, at registration, so downstream children
* inherit it with a single lookup rather than walking the ancestor chain.
*
* @param {import('../opentracing/span')} span
* @param {import('../opentracing/span')} [parent] the LLMObs parent span, if any
*/
// TODO: spans whose kind changes after registration (e.g. claude-agent-sdk tools promoted to
// sub-agents) will not retroactively update already-finished children's attribution. Follow up.
#tagAgentAttribution (span, parent) {
let name, spanId
let name, spanId, version
if (registry.has(parent)) {
// Local LLMObs parent: attribute to it if it is an agent, else inherit its resolution.
({ name, spanId } = resolveAgentAttribution(registry.get(parent), parent))
({ name, spanId, version } = resolveAgentAttribution(registry.get(parent), parent))
} else if (span.context()._trace.tags[PROPAGATED_PARENT_ID_KEY]) {
// Distributed LLMObs parent: inherit the nearest agent propagated from upstream. The
// name may be absent when the upstream hop ran an older SDK or its name was not
Expand All @@ -300,6 +309,7 @@ class LLMObsTagger {

if (name != null) this._setTag(span, PARENT_AGENT_NAME, name)
if (spanId != null) this._setTag(span, PARENT_AGENT_SPAN_ID, spanId)
if (version) this._setTag(span, PARENT_AGENT_VERSION, version)
}

// TODO: similarly for the following `tag` methods,
Expand Down
14 changes: 10 additions & 4 deletions packages/dd-trace/src/llmobs/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ const {
SPAN_KINDS,
SPAN_KIND,
NAME,
AGENT_VERSION,
PARENT_AGENT_NAME,
PARENT_AGENT_SPAN_ID,
PARENT_AGENT_VERSION,
} = require('./constants/tags')

const DECIMAL_TRACE_ID_REGEX = /^\d+$/
Expand Down Expand Up @@ -326,14 +328,18 @@ function agentNameWireSafe (name) {
*
* @param {Record<string, unknown> | undefined} tags - Registry entry for the parent span.
* @param {import('../opentracing/span')} [span] - The parent span itself (needed for span id / name).
* @returns {{ name: string | undefined, spanId: string | undefined }}
* @returns {{ name: string | undefined, spanId: string | undefined, version: string | undefined }}
*/
function resolveAgentAttribution (tags, span) {
if (!tags) return { name: undefined, spanId: undefined }
if (!tags) return { name: undefined, spanId: undefined, version: undefined }
if (tags[SPAN_KIND] === 'agent') {
return { name: tags[NAME] || span._name, spanId: span.context().toSpanId() }
return {
name: tags[NAME] || span._name,
spanId: span.context().toSpanId(),
version: tags[AGENT_VERSION] || tags[PARENT_AGENT_VERSION],
}
}
return { name: tags[PARENT_AGENT_NAME], spanId: tags[PARENT_AGENT_SPAN_ID] }
return { name: tags[PARENT_AGENT_NAME], spanId: tags[PARENT_AGENT_SPAN_ID], version: tags[PARENT_AGENT_VERSION] }
}

function spanHasError (span) {
Expand Down
Loading
Loading