Skip to content
10 changes: 10 additions & 0 deletions TELEMETRY-CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,16 @@ them so the graph span and the `invoke_agent` root share one name for the graph
everything LaunchDarkly owns on a span sits under `launchdarkly.`. `ld.ai.` stays reserved for
metric and event keys.

Every `launchdarkly.graph` span, from `graph()` and from each `native_graph` adapter, goes through
`set_ld_span_attributes`, so it carries the identity in section 2 and the `feature_flag` event.
Its config key is the graph key. The graph-level events (`$ld:ai:graph:invocation_success`,
`invocation_failure`, `duration:total` and `total_tokens`) use the same track data, so their
`configKey` is the graph key too, whichever runner produced them. Node-level events keep the node's
own config key.

A native adapter has no graph variation metadata, so its graph span and graph events report an
empty `variationKey` and version 1. `graph()` reports the real values.

Nothing else in `graph.py` or `native_graph.py` needs work.

---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
get_client,
make_track_data,
report_usage,
set_ld_span_attributes,
to_ld_context,
)
from launchdarkly_ai_server.utils import make_graph_track_data

from ._version import PACKAGE_NAME, __version__

Expand Down Expand Up @@ -240,20 +242,30 @@ async def invoke(
)
raw_handlers: dict[str, Any] = _opts.get("tool_handlers") or {}

start_time = time.monotonic()
run_id = str(uuid.uuid4())
tracer_name = "@launchdarkly/ai-claude-agents"
if _HAS_OTEL:
span = trace.get_tracer(tracer_name).start_span("launchdarkly.graph")
span.set_attribute("launchdarkly.graph.key", def_obj.key)
else:
span = None

start_time = time.monotonic()
run_id = str(uuid.uuid4())
path: list[str] = []
total_usage = {"input": 0, "output": 0, "total": 0}
subagent_tool_ctx: dict[str, Any] = {}

# One try covers setup and the run, so a setup error (a sub-agent tool
# that fails to build) is recorded on the span and tracked as an
# invocation failure, like a run error. The span ends exactly once, in
# the finally.
try:
if span:
set_ld_span_attributes(
span,
{
"__ld": make_graph_track_data(def_obj.key, run_id),
"ldContext": raw_ld_context,
},
)

async def _build_node(node: GraphNode) -> None:
if node.key == root.key:
Expand Down Expand Up @@ -355,29 +367,17 @@ async def _subagent_execute(
)
root_start = time.monotonic()

try:
result = await _run_query(
root,
input_text,
vs,
raw_handlers,
ld_context,
def_obj.key,
run_id,
root_child_tools,
history,
)
except Exception as exc:
if span:
span.record_exception(exc)
span.set_status(SpanStatusCode.ERROR, str(exc))
span.end()
if ld_context:
td = make_track_data(root, def_obj.key, run_id)
get_client().track(
"$ld:ai:graph:invocation_failure", ld_context, td, 1
)
raise
result = await _run_query(
root,
input_text,
vs,
raw_handlers,
ld_context,
def_obj.key,
run_id,
root_child_tools,
history,
)

final_output = result["output"]
root_usage = result["usage"]
Expand Down Expand Up @@ -411,30 +411,38 @@ async def _subagent_execute(
span.set_attribute("gen_ai.usage.input_tokens", total_usage["input"])
span.set_attribute("gen_ai.usage.output_tokens", total_usage["output"])
span.set_attribute("gen_ai.usage.total_tokens", total_usage["total"])
span.set_status(SpanStatusCode.OK)
span.end()

if ld_context:
root_td = make_track_data(root, def_obj.key, run_id)
graph_td = make_graph_track_data(def_obj.key, run_id)
client = get_client()
client.track(
"$ld:ai:graph:duration:total", ld_context, root_td, graph_dur
"$ld:ai:graph:duration:total", ld_context, graph_td, graph_dur
)
client.track(
"$ld:ai:graph:total_tokens",
ld_context,
root_td,
graph_td,
total_usage["total"],
)
client.track("$ld:ai:graph:invocation_success", ld_context, root_td, 1)
client.track("$ld:ai:graph:invocation_success", ld_context, graph_td, 1)

if span:
span.set_status(SpanStatusCode.OK)
return {"response": final_output, "usage": total_usage}

except Exception as exc:
if span:
span.record_exception(exc)
span.set_status(SpanStatusCode.ERROR, str(exc))
span.end()
if ld_context:
get_client().track(
"$ld:ai:graph:invocation_failure",
ld_context,
make_graph_track_data(def_obj.key, run_id),
1,
)
raise
finally:
if span:
span.end()

return types.SimpleNamespace(invoke=invoke)
Loading
Loading