agentloop-profiler 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
agentloop/otel.py
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agentloop.events import AgentEvent
|
|
9
|
+
from agentloop.operations import LEGACY_OPERATION_KINDS, normalize_operation_kind
|
|
10
|
+
from agentloop.otel_ids import to_span_id, to_trace_id
|
|
11
|
+
from agentloop.otel_semantics import (
|
|
12
|
+
INPUT_USAGE,
|
|
13
|
+
OUTPUT_USAGE,
|
|
14
|
+
evaluation_result,
|
|
15
|
+
extended_kind,
|
|
16
|
+
normalize_metadata,
|
|
17
|
+
usage_count,
|
|
18
|
+
)
|
|
19
|
+
from agentloop.schema import TraceValidationError
|
|
20
|
+
from agentloop.tokens import PROVIDER, UNAVAILABLE
|
|
21
|
+
from agentloop.tracer import AgentTrace
|
|
22
|
+
from agentloop.version import __version__
|
|
23
|
+
|
|
24
|
+
# Prefix under which AgentLoop exports user event metadata as span attributes.
|
|
25
|
+
_METADATA_PREFIX = "agentloop.metadata."
|
|
26
|
+
|
|
27
|
+
_NANOSECONDS_PER_MILLISECOND = 1_000_000
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def trace_from_otel(
|
|
31
|
+
payload: dict[str, Any] | list[dict[str, Any]], name: str | None = None
|
|
32
|
+
) -> AgentTrace:
|
|
33
|
+
"""Convert a single-trace OTLP/GenAI-style JSON payload into an AgentLoop trace.
|
|
34
|
+
|
|
35
|
+
This accepts the standard OTLP JSON shape (`resourceSpans -> scopeSpans -> spans`),
|
|
36
|
+
a dictionary with a top-level `spans` list, or a raw list of span dictionaries.
|
|
37
|
+
|
|
38
|
+
The payload must describe **one** trace. OTLP exporters routinely batch spans
|
|
39
|
+
from several traces into one request; passing such a batch here raises
|
|
40
|
+
:class:`agentloop.schema.TraceValidationError` rather than silently collapsing
|
|
41
|
+
the traces into one. Use :func:`traces_from_otel` for batches. An empty payload
|
|
42
|
+
yields an empty trace, preserving the historical single-trace behavior.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
traces = traces_from_otel(payload, name=name)
|
|
46
|
+
if len(traces) > 1:
|
|
47
|
+
raise TraceValidationError(
|
|
48
|
+
"traceId",
|
|
49
|
+
f"payload contains {len(traces)} distinct traces; use traces_from_otel() "
|
|
50
|
+
"to import a batch",
|
|
51
|
+
)
|
|
52
|
+
if not traces:
|
|
53
|
+
return _build_trace([], {}, name=name)
|
|
54
|
+
return traces[0]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def traces_from_otel(
|
|
58
|
+
payload: dict[str, Any] | list[dict[str, Any]], name: str | None = None
|
|
59
|
+
) -> list[AgentTrace]:
|
|
60
|
+
"""Convert an OTLP/GenAI-style batch into one AgentLoop trace per trace id.
|
|
61
|
+
|
|
62
|
+
Spans are grouped by their ``traceId`` in first-seen order regardless of how
|
|
63
|
+
they are interleaved in the payload, so a batch of unrelated traces keeps its
|
|
64
|
+
boundaries. Parent links are resolved within each trace only. Spans with no
|
|
65
|
+
trace id are grouped together into a single trace. Accepts the same input
|
|
66
|
+
shapes as :func:`trace_from_otel`.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
groups: dict[str, list[dict[str, Any]]] = {}
|
|
70
|
+
group_resource: dict[str, dict[str, Any]] = {}
|
|
71
|
+
for resource_attrs, spans in _iter_resource_blocks(payload):
|
|
72
|
+
if not spans and _native_run_id(resource_attrs):
|
|
73
|
+
key = to_trace_id(_native_run_id(resource_attrs))
|
|
74
|
+
groups.setdefault(key, [])
|
|
75
|
+
group_resource[key] = resource_attrs
|
|
76
|
+
block_trace_keys = {_trace_key(span) for span in spans}
|
|
77
|
+
block_is_single_trace = len(block_trace_keys) == 1
|
|
78
|
+
for span in spans:
|
|
79
|
+
key = _trace_key(span)
|
|
80
|
+
groups.setdefault(key, []).append(span)
|
|
81
|
+
# Only adopt a resource block's native identity when that block holds
|
|
82
|
+
# exactly one trace; a multi-trace block cannot name every trace.
|
|
83
|
+
if key not in group_resource:
|
|
84
|
+
group_resource[key] = resource_attrs if block_is_single_trace else {}
|
|
85
|
+
elif not block_is_single_trace or group_resource[key] != resource_attrs:
|
|
86
|
+
group_resource[key] = {}
|
|
87
|
+
traces = [
|
|
88
|
+
_build_trace(spans, group_resource.get(key, {}), name=name) for key, spans in groups.items()
|
|
89
|
+
]
|
|
90
|
+
by_id = dict(zip(groups, traces))
|
|
91
|
+
events_by_trace = {
|
|
92
|
+
key: {event.metadata.get("otel_span_id"): event for event in trace.events}
|
|
93
|
+
for key, trace in by_id.items()
|
|
94
|
+
}
|
|
95
|
+
if isinstance(payload, dict):
|
|
96
|
+
for resource in payload.get("resourceLogs", []):
|
|
97
|
+
for scope in resource.get("scopeLogs", []):
|
|
98
|
+
for record in scope.get("logRecords", []):
|
|
99
|
+
key = _trace_key(record)
|
|
100
|
+
if key not in by_id:
|
|
101
|
+
trace = AgentTrace(
|
|
102
|
+
name=name or "otel_log_trace",
|
|
103
|
+
run_id="run_" + key if key else "run_otel_logs",
|
|
104
|
+
metadata={"source": "otel", "execution_data_present": False},
|
|
105
|
+
elapsed_ms=0,
|
|
106
|
+
)
|
|
107
|
+
by_id[key] = trace
|
|
108
|
+
traces.append(trace)
|
|
109
|
+
trace = by_id[key]
|
|
110
|
+
event = events_by_trace.get(key, {}).get(record.get("spanId"))
|
|
111
|
+
metadata = event.metadata if event is not None else trace.metadata
|
|
112
|
+
metadata.setdefault("otel_log_records", []).append(
|
|
113
|
+
{
|
|
114
|
+
"record": deepcopy(record),
|
|
115
|
+
"resource": deepcopy(resource.get("resource", {})),
|
|
116
|
+
"scope": deepcopy(scope.get("scope", {})),
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
result = evaluation_result(
|
|
120
|
+
_attributes(record), source=str(record.get("eventName") or "otel_log")
|
|
121
|
+
)
|
|
122
|
+
if result:
|
|
123
|
+
metadata.setdefault("evaluation_results", []).append(result)
|
|
124
|
+
return traces
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _build_trace(
|
|
128
|
+
spans: list[dict[str, Any]], resource_attrs: dict[str, Any], *, name: str | None
|
|
129
|
+
) -> AgentTrace:
|
|
130
|
+
run_id = _native_run_id(resource_attrs) or _run_id_from_spans(spans)
|
|
131
|
+
trace_name = name or _native_trace_name(resource_attrs) or _trace_name(spans)
|
|
132
|
+
native_metadata = resource_attrs.get("agentloop.trace.metadata")
|
|
133
|
+
trace = AgentTrace(
|
|
134
|
+
name=trace_name,
|
|
135
|
+
run_id=run_id,
|
|
136
|
+
metadata=deepcopy(native_metadata)
|
|
137
|
+
if isinstance(native_metadata, dict)
|
|
138
|
+
else {"source": "otel"},
|
|
139
|
+
)
|
|
140
|
+
for span in spans:
|
|
141
|
+
trace.add_event(_event_from_span(span, run_id))
|
|
142
|
+
bounds = _trace_bounds_ns(spans)
|
|
143
|
+
if bounds is not None:
|
|
144
|
+
started_ns, ended_ns = bounds
|
|
145
|
+
trace.started_at = _iso_from_ns(started_ns)
|
|
146
|
+
trace.ended_at = _iso_from_ns(ended_ns)
|
|
147
|
+
trace.elapsed_ms = (ended_ns - started_ns) / _NANOSECONDS_PER_MILLISECOND
|
|
148
|
+
if "agentloop.trace.started_at" in resource_attrs:
|
|
149
|
+
trace.started_at = resource_attrs["agentloop.trace.started_at"]
|
|
150
|
+
trace.ended_at = resource_attrs.get("agentloop.trace.ended_at")
|
|
151
|
+
trace.elapsed_ms = resource_attrs.get("agentloop.trace.elapsed_ms")
|
|
152
|
+
if trace.elapsed_ms is not None:
|
|
153
|
+
trace.elapsed_ms = _nonnegative_timing(trace.elapsed_ms, "agentloop.trace.elapsed_ms")
|
|
154
|
+
return trace
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def trace_to_otel(trace: AgentTrace) -> dict[str, Any]:
|
|
158
|
+
"""Export an AgentLoop trace as dependency-free OTLP-like GenAI JSON."""
|
|
159
|
+
|
|
160
|
+
spans = [_span_from_event(trace, event) for event in trace.events]
|
|
161
|
+
return {
|
|
162
|
+
"resourceSpans": [
|
|
163
|
+
{
|
|
164
|
+
"resource": {
|
|
165
|
+
"attributes": [
|
|
166
|
+
_attribute("service.name", "agentloop"),
|
|
167
|
+
_attribute("agentloop.trace.name", trace.name),
|
|
168
|
+
_attribute("agentloop.run_id", trace.run_id),
|
|
169
|
+
_attribute("agentloop.trace.metadata", trace.metadata),
|
|
170
|
+
_attribute("agentloop.trace.started_at", trace.started_at),
|
|
171
|
+
_attribute("agentloop.trace.ended_at", trace.ended_at),
|
|
172
|
+
_attribute("agentloop.trace.elapsed_ms", trace.elapsed_ms),
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
"scopeSpans": [
|
|
176
|
+
{
|
|
177
|
+
"scope": {"name": "agentloop", "version": __version__},
|
|
178
|
+
"spans": spans,
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _iter_resource_blocks(
|
|
187
|
+
payload: dict[str, Any] | list[dict[str, Any]],
|
|
188
|
+
) -> list[tuple[dict[str, Any], list[dict[str, Any]]]]:
|
|
189
|
+
"""Return ``(resource_attributes, spans)`` blocks, preserving payload order.
|
|
190
|
+
|
|
191
|
+
Resource attributes carry the AgentLoop-native identity that
|
|
192
|
+
:func:`trace_to_otel` writes (trace name and run id). Raw span lists and
|
|
193
|
+
top-level ``spans`` payloads have no resource, so their attributes are empty.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
if isinstance(payload, list):
|
|
197
|
+
return [({}, list(payload))]
|
|
198
|
+
if "spans" in payload and isinstance(payload["spans"], list):
|
|
199
|
+
return [(_resource_attributes(payload.get("resource")), list(payload["spans"]))]
|
|
200
|
+
|
|
201
|
+
blocks: list[tuple[dict[str, Any], list[dict[str, Any]]]] = []
|
|
202
|
+
for resource_span in payload.get("resourceSpans", []):
|
|
203
|
+
resource_attrs = _resource_attributes(resource_span.get("resource"))
|
|
204
|
+
spans: list[dict[str, Any]] = []
|
|
205
|
+
for scope_span in resource_span.get("scopeSpans", []):
|
|
206
|
+
for span in scope_span.get("spans", []):
|
|
207
|
+
spans.append(
|
|
208
|
+
{
|
|
209
|
+
**span,
|
|
210
|
+
"_resource_attributes": resource_attrs,
|
|
211
|
+
"_scope": scope_span.get("scope", {}),
|
|
212
|
+
"_schema_url": scope_span.get("schemaUrl", resource_span.get("schemaUrl")),
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
blocks.append((resource_attrs, spans))
|
|
216
|
+
return blocks
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _resource_attributes(resource: Any) -> dict[str, Any]:
|
|
220
|
+
if not isinstance(resource, dict):
|
|
221
|
+
return {}
|
|
222
|
+
return _attributes(resource)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _trace_key(span: dict[str, Any]) -> str:
|
|
226
|
+
return str(span.get("traceId") or span.get("trace_id") or "").lower()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _native_run_id(resource_attrs: dict[str, Any]) -> str | None:
|
|
230
|
+
run_id = resource_attrs.get("agentloop.run_id")
|
|
231
|
+
return str(run_id) if run_id else None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _native_trace_name(resource_attrs: dict[str, Any]) -> str | None:
|
|
235
|
+
name = resource_attrs.get("agentloop.trace.name")
|
|
236
|
+
return str(name) if name else None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _run_id_from_spans(spans: list[dict[str, Any]]) -> str:
|
|
240
|
+
for span in spans:
|
|
241
|
+
trace_id = str(span.get("traceId") or span.get("trace_id") or "")
|
|
242
|
+
if trace_id:
|
|
243
|
+
# Preserve the full trace id (not just the last 16 chars) so a valid
|
|
244
|
+
# OTLP id round-trips back out unchanged via to_trace_id().
|
|
245
|
+
return "run_" + trace_id.lower()
|
|
246
|
+
return "run_otel_import"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _trace_name(spans: list[dict[str, Any]]) -> str:
|
|
250
|
+
for span in spans:
|
|
251
|
+
attrs = _attributes(span)
|
|
252
|
+
workflow = attrs.get("gen_ai.workflow.name") or attrs.get("agentloop.trace.name")
|
|
253
|
+
if workflow:
|
|
254
|
+
return str(workflow)
|
|
255
|
+
return "otel_trace"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _event_from_span(span: dict[str, Any], run_id: str) -> AgentEvent:
|
|
259
|
+
attrs = _attributes(span)
|
|
260
|
+
operation = str(attrs.get("gen_ai.operation.name") or attrs.get("agentloop.event_type") or "")
|
|
261
|
+
event_type = str(attrs.get("agentloop.event_type") or _event_type(operation, span.get("name")))
|
|
262
|
+
kind = extended_kind(attrs)
|
|
263
|
+
if kind is not None and "agentloop.event_type" not in attrs:
|
|
264
|
+
event_type = "model_call" if kind == "model" else "tool_call"
|
|
265
|
+
started_ns = _int_or_none(span.get("startTimeUnixNano") or span.get("start_time_unix_nano"))
|
|
266
|
+
ended_ns = _int_or_none(span.get("endTimeUnixNano") or span.get("end_time_unix_nano"))
|
|
267
|
+
duration_ms = _duration_ms(started_ns, ended_ns, span)
|
|
268
|
+
if "agentloop.duration_ms" in attrs:
|
|
269
|
+
duration_ms = _nonnegative_timing(attrs["agentloop.duration_ms"], "agentloop.duration_ms")
|
|
270
|
+
span_id = str(span.get("spanId") or span.get("span_id") or "")
|
|
271
|
+
parent_span_id = str(span.get("parentSpanId") or span.get("parent_span_id") or "") or None
|
|
272
|
+
status = _status(span)
|
|
273
|
+
error = _error(span)
|
|
274
|
+
|
|
275
|
+
metadata = _user_metadata(attrs, span, span_id)
|
|
276
|
+
if "operation_kind" not in metadata:
|
|
277
|
+
if "agentloop.operation_kind" in attrs:
|
|
278
|
+
metadata["operation_kind"] = attrs["agentloop.operation_kind"]
|
|
279
|
+
elif "agentloop.event_type" not in attrs:
|
|
280
|
+
metadata["operation_kind"] = kind or _import_operation_kind(operation, event_type)
|
|
281
|
+
normalize_metadata(attrs, metadata)
|
|
282
|
+
_preserve_span_details(span, attrs, metadata)
|
|
283
|
+
|
|
284
|
+
# Restore AgentLoop-native ids when the exporter preserved them, so repeated
|
|
285
|
+
# round trips keep stable event and parent identity (issue #63). Fall back to
|
|
286
|
+
# the OTLP span ids for third-party payloads.
|
|
287
|
+
native_event_id = attrs.get("agentloop.native_event_id")
|
|
288
|
+
event_id = str(native_event_id) if native_event_id else ("span_" + span_id if span_id else "")
|
|
289
|
+
native_parent_id = attrs.get("agentloop.native_parent_id")
|
|
290
|
+
if native_parent_id:
|
|
291
|
+
parent_id = str(native_parent_id)
|
|
292
|
+
elif parent_span_id:
|
|
293
|
+
parent_id = "span_" + parent_span_id
|
|
294
|
+
else:
|
|
295
|
+
parent_id = None
|
|
296
|
+
|
|
297
|
+
return AgentEvent(
|
|
298
|
+
event_id=event_id,
|
|
299
|
+
run_id=run_id,
|
|
300
|
+
event_type=event_type,
|
|
301
|
+
name=str(
|
|
302
|
+
attrs.get("gen_ai.tool.name")
|
|
303
|
+
or attrs.get("tool.name")
|
|
304
|
+
or attrs.get("agentloop.name")
|
|
305
|
+
or span.get("name")
|
|
306
|
+
or event_type
|
|
307
|
+
),
|
|
308
|
+
started_at=_iso_from_ns(started_ns),
|
|
309
|
+
ended_at=_iso_from_ns(ended_ns),
|
|
310
|
+
duration_ms=duration_ms,
|
|
311
|
+
parent_id=parent_id,
|
|
312
|
+
model=_first_string(
|
|
313
|
+
attrs,
|
|
314
|
+
"agentloop.model",
|
|
315
|
+
"gen_ai.response.model",
|
|
316
|
+
"gen_ai.request.model",
|
|
317
|
+
"llm.response.model_name",
|
|
318
|
+
"llm.model_name",
|
|
319
|
+
"llm.request.model_name",
|
|
320
|
+
"embedding.model_name",
|
|
321
|
+
),
|
|
322
|
+
input_tokens=usage_count(attrs, ("agentloop.input_tokens", *INPUT_USAGE)),
|
|
323
|
+
output_tokens=usage_count(attrs, ("agentloop.output_tokens", *OUTPUT_USAGE)),
|
|
324
|
+
token_provenance=_token_provenance(attrs),
|
|
325
|
+
status=status,
|
|
326
|
+
error=error,
|
|
327
|
+
metadata=metadata,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _span_from_event(trace: AgentTrace, event: AgentEvent) -> dict[str, Any]:
|
|
332
|
+
start_ns = _ns_from_iso(event.started_at)
|
|
333
|
+
end_ns = _ns_from_iso(event.ended_at)
|
|
334
|
+
generic = event.event_type not in LEGACY_OPERATION_KINDS
|
|
335
|
+
attrs = (
|
|
336
|
+
[]
|
|
337
|
+
if generic
|
|
338
|
+
else [
|
|
339
|
+
_attribute(
|
|
340
|
+
"gen_ai.operation.name", _operation_name(event.event_type, event.operation_kind)
|
|
341
|
+
)
|
|
342
|
+
]
|
|
343
|
+
) + [
|
|
344
|
+
_attribute("agentloop.event_type", event.event_type),
|
|
345
|
+
_attribute("agentloop.name", event.name),
|
|
346
|
+
_attribute("agentloop.duration_ms", event.duration_ms),
|
|
347
|
+
_attribute("agentloop.run_id", trace.run_id),
|
|
348
|
+
_attribute(
|
|
349
|
+
"agentloop.input_tokens" if generic else "gen_ai.usage.input_tokens", event.input_tokens
|
|
350
|
+
),
|
|
351
|
+
_attribute(
|
|
352
|
+
"agentloop.output_tokens" if generic else "gen_ai.usage.output_tokens",
|
|
353
|
+
event.output_tokens,
|
|
354
|
+
),
|
|
355
|
+
]
|
|
356
|
+
if "operation_kind" in event.metadata:
|
|
357
|
+
attrs.append(_attribute("agentloop.operation_kind", event.metadata["operation_kind"]))
|
|
358
|
+
preserved_fields = sorted(
|
|
359
|
+
set(event.metadata) & (_reserved_metadata_keys() - {"otel_span_id", "otel_trace_id"})
|
|
360
|
+
)
|
|
361
|
+
if preserved_fields:
|
|
362
|
+
attrs.append(_attribute("agentloop.preserved_span_fields", preserved_fields))
|
|
363
|
+
if event.model:
|
|
364
|
+
attrs.append(
|
|
365
|
+
_attribute("agentloop.model" if generic else "gen_ai.request.model", event.model)
|
|
366
|
+
)
|
|
367
|
+
attrs.append(_attribute("agentloop.token_provenance", event.token_provenance))
|
|
368
|
+
for key, value in sorted((event.metadata or {}).items()):
|
|
369
|
+
if key in _reserved_metadata_keys():
|
|
370
|
+
# Transport diagnostics (otel_span_id/otel_trace_id) are re-derived on
|
|
371
|
+
# import; re-exporting them would let user metadata grow each round trip.
|
|
372
|
+
continue
|
|
373
|
+
attrs.append(_attribute(f"{_METADATA_PREFIX}{key}", value))
|
|
374
|
+
|
|
375
|
+
# Keep the original native ids in attributes so a remapped (non-hex/custom)
|
|
376
|
+
# id stays diagnosable; the run id is already carried as agentloop.run_id.
|
|
377
|
+
attrs.append(_attribute("agentloop.native_event_id", event.event_id))
|
|
378
|
+
if event.parent_id:
|
|
379
|
+
attrs.append(_attribute("agentloop.native_parent_id", event.parent_id))
|
|
380
|
+
|
|
381
|
+
span = {
|
|
382
|
+
"traceId": to_trace_id(trace.run_id),
|
|
383
|
+
"spanId": to_span_id(event.event_id),
|
|
384
|
+
"name": event.name,
|
|
385
|
+
"kind": event.metadata.get("otel_span_kind", "SPAN_KIND_INTERNAL"),
|
|
386
|
+
"startTimeUnixNano": str(start_ns),
|
|
387
|
+
"endTimeUnixNano": str(end_ns),
|
|
388
|
+
"attributes": attrs,
|
|
389
|
+
"status": {"code": "STATUS_CODE_OK" if event.status == "ok" else "STATUS_CODE_ERROR"},
|
|
390
|
+
}
|
|
391
|
+
if event.parent_id:
|
|
392
|
+
span["parentSpanId"] = to_span_id(event.parent_id)
|
|
393
|
+
if event.error:
|
|
394
|
+
span["status"]["message"] = event.error
|
|
395
|
+
for source, target in (
|
|
396
|
+
("otel_links", "links"),
|
|
397
|
+
("otel_events", "events"),
|
|
398
|
+
("otel_trace_state", "traceState"),
|
|
399
|
+
("otel_span_flags", "flags"),
|
|
400
|
+
):
|
|
401
|
+
if source in event.metadata:
|
|
402
|
+
span[target] = deepcopy(event.metadata[source])
|
|
403
|
+
return span
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _user_metadata(attrs: dict[str, Any], span: dict[str, Any], span_id: str) -> dict[str, Any]:
|
|
407
|
+
"""Build user event metadata from span attributes (issue #63).
|
|
408
|
+
|
|
409
|
+
Decodes the ``agentloop.metadata.`` namespace exactly once, preserves genuine
|
|
410
|
+
third-party attributes, and drops AgentLoop transport/native-id and gen_ai
|
|
411
|
+
bookkeeping keys so they never leak into or accumulate in user metadata.
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
decoded: dict[str, Any] = {}
|
|
415
|
+
passthrough: dict[str, Any] = {}
|
|
416
|
+
for key, value in attrs.items():
|
|
417
|
+
if key.startswith(_METADATA_PREFIX):
|
|
418
|
+
decoded[key[len(_METADATA_PREFIX) :]] = value
|
|
419
|
+
elif "agentloop.event_type" not in attrs and key == "gen_ai.operation.name":
|
|
420
|
+
passthrough[key] = value
|
|
421
|
+
elif "agentloop.event_type" in attrs and key in _USAGE_ATTRIBUTE_KEYS:
|
|
422
|
+
continue
|
|
423
|
+
elif key in _direct_attribute_keys() or key in _transport_attribute_keys():
|
|
424
|
+
continue
|
|
425
|
+
else:
|
|
426
|
+
passthrough[key] = value # third-party attribute, preserved as-is
|
|
427
|
+
# Native AgentLoop metadata wins over any colliding third-party attribute.
|
|
428
|
+
metadata = {**passthrough, **decoded}
|
|
429
|
+
# Transport diagnostics: available on the imported event but never re-exported.
|
|
430
|
+
metadata["otel_span_id"] = span_id
|
|
431
|
+
metadata["otel_trace_id"] = span.get("traceId") or span.get("trace_id")
|
|
432
|
+
return metadata
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _preserve_span_details(
|
|
436
|
+
span: dict[str, Any], attrs: dict[str, Any], metadata: dict[str, Any]
|
|
437
|
+
) -> None:
|
|
438
|
+
native = "agentloop.event_type" in attrs
|
|
439
|
+
preserved = attrs.get("agentloop.preserved_span_fields", [])
|
|
440
|
+
for source, target in (
|
|
441
|
+
("links", "otel_links"),
|
|
442
|
+
("events", "otel_events"),
|
|
443
|
+
("traceState", "otel_trace_state"),
|
|
444
|
+
("flags", "otel_span_flags"),
|
|
445
|
+
("kind", "otel_span_kind"),
|
|
446
|
+
):
|
|
447
|
+
if source in span and (not native or target in preserved):
|
|
448
|
+
metadata.setdefault(target, deepcopy(span[source]))
|
|
449
|
+
if not native:
|
|
450
|
+
resource = span.get("_resource_attributes")
|
|
451
|
+
if resource:
|
|
452
|
+
metadata.setdefault("otel_resource_attributes", deepcopy(resource))
|
|
453
|
+
if span.get("_scope"):
|
|
454
|
+
metadata.setdefault("otel_scope", deepcopy(span["_scope"]))
|
|
455
|
+
if span.get("_schema_url"):
|
|
456
|
+
metadata.setdefault("otel_schema_url", span["_schema_url"])
|
|
457
|
+
evaluations = []
|
|
458
|
+
result = evaluation_result(attrs, source="span_attributes")
|
|
459
|
+
if result:
|
|
460
|
+
evaluations.append(result)
|
|
461
|
+
for event in span.get("events", []):
|
|
462
|
+
result = evaluation_result(_attributes(event), source=str(event.get("name", "span_event")))
|
|
463
|
+
if result:
|
|
464
|
+
evaluations.append(result)
|
|
465
|
+
if evaluations:
|
|
466
|
+
metadata.setdefault("evaluation_results", evaluations)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _transport_attribute_keys() -> set[str]:
|
|
470
|
+
return {
|
|
471
|
+
"agentloop.run_id",
|
|
472
|
+
"agentloop.native_event_id",
|
|
473
|
+
"agentloop.native_parent_id",
|
|
474
|
+
"agentloop.preserved_span_fields",
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _reserved_metadata_keys() -> set[str]:
|
|
479
|
+
return {
|
|
480
|
+
"otel_span_id",
|
|
481
|
+
"otel_trace_id",
|
|
482
|
+
"otel_links",
|
|
483
|
+
"otel_events",
|
|
484
|
+
"otel_trace_state",
|
|
485
|
+
"otel_span_flags",
|
|
486
|
+
"otel_span_kind",
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _event_type(operation: str, name: Any) -> str:
|
|
491
|
+
op = operation.lower()
|
|
492
|
+
span_name = str(name or "").lower()
|
|
493
|
+
if op in {"chat", "text_completion", "embeddings", "generate_content"}:
|
|
494
|
+
return "model_call"
|
|
495
|
+
if op in {"execute_tool", "invoke_agent", "create_agent", "invoke_workflow", "retrieval"}:
|
|
496
|
+
return "tool_call"
|
|
497
|
+
if op in LEGACY_OPERATION_KINDS:
|
|
498
|
+
return op
|
|
499
|
+
if "tool" in span_name:
|
|
500
|
+
return "tool_call"
|
|
501
|
+
if op == "retry" or "retry" in span_name:
|
|
502
|
+
return "retry"
|
|
503
|
+
return "tool_call" if op in {"invoke_agent", "invoke_workflow"} else "model_call"
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _import_operation_kind(operation: str, event_type: str) -> str:
|
|
507
|
+
mapping = {
|
|
508
|
+
"chat": "model",
|
|
509
|
+
"text_completion": "model",
|
|
510
|
+
"embeddings": "model",
|
|
511
|
+
"generate_content": "model",
|
|
512
|
+
"execute_tool": "tool",
|
|
513
|
+
"invoke_agent": "agent",
|
|
514
|
+
"create_agent": "agent",
|
|
515
|
+
"invoke_workflow": "workflow",
|
|
516
|
+
"retrieval": "retriever",
|
|
517
|
+
**LEGACY_OPERATION_KINDS,
|
|
518
|
+
}
|
|
519
|
+
if not operation:
|
|
520
|
+
return LEGACY_OPERATION_KINDS.get(event_type, "unknown")
|
|
521
|
+
normalized = normalize_operation_kind(operation)
|
|
522
|
+
return mapping.get(
|
|
523
|
+
operation.strip().lower(), operation if normalized == "unknown" else normalized
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _operation_name(event_type: str, kind: str) -> str:
|
|
528
|
+
if kind in {"agent", "workflow", "retriever"}:
|
|
529
|
+
return {"agent": "invoke_agent", "workflow": "invoke_workflow", "retriever": "retrieval"}[
|
|
530
|
+
kind
|
|
531
|
+
]
|
|
532
|
+
if kind in {"memory", "reranker", "guardrail", "evaluator", "retry"}:
|
|
533
|
+
return kind
|
|
534
|
+
if event_type == "model_call":
|
|
535
|
+
return "chat"
|
|
536
|
+
if event_type == "tool_call":
|
|
537
|
+
return "execute_tool"
|
|
538
|
+
return event_type
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _attributes(span: dict[str, Any]) -> dict[str, Any]:
|
|
542
|
+
raw = span.get("attributes", {})
|
|
543
|
+
if isinstance(raw, dict):
|
|
544
|
+
return raw.copy()
|
|
545
|
+
attrs: dict[str, Any] = {}
|
|
546
|
+
for item in raw or []:
|
|
547
|
+
key = item.get("key")
|
|
548
|
+
if key:
|
|
549
|
+
attrs[key] = _attribute_value(item.get("value", {}))
|
|
550
|
+
return attrs
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _attribute_value(value: dict[str, Any]) -> Any:
|
|
554
|
+
if not isinstance(value, dict):
|
|
555
|
+
return value
|
|
556
|
+
if "intValue" in value:
|
|
557
|
+
raw = value["intValue"]
|
|
558
|
+
if isinstance(raw, bool) or not isinstance(raw, int | str):
|
|
559
|
+
raise TraceValidationError("intValue", "must be an integer or decimal integer string")
|
|
560
|
+
try:
|
|
561
|
+
return int(raw)
|
|
562
|
+
except ValueError as exc:
|
|
563
|
+
raise TraceValidationError(
|
|
564
|
+
"intValue", "must be an integer or decimal integer string"
|
|
565
|
+
) from exc
|
|
566
|
+
for key in ("stringValue", "doubleValue", "boolValue"):
|
|
567
|
+
if key in value:
|
|
568
|
+
return value[key]
|
|
569
|
+
if "arrayValue" in value:
|
|
570
|
+
values = value.get("arrayValue", {}).get("values", [])
|
|
571
|
+
return [_attribute_value(item) for item in values]
|
|
572
|
+
if "kvlistValue" in value:
|
|
573
|
+
return {
|
|
574
|
+
item["key"]: _attribute_value(item.get("value", {}))
|
|
575
|
+
for item in value["kvlistValue"].get("values", [])
|
|
576
|
+
}
|
|
577
|
+
if "bytesValue" in value:
|
|
578
|
+
return {"otel_bytes_base64": value["bytesValue"]}
|
|
579
|
+
if value:
|
|
580
|
+
return {"otel_unrecognized_value": deepcopy(value)}
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _attribute(key: str, value: Any) -> dict[str, Any]:
|
|
585
|
+
if value is None:
|
|
586
|
+
encoded = {}
|
|
587
|
+
elif isinstance(value, dict):
|
|
588
|
+
encoded = {
|
|
589
|
+
"kvlistValue": {"values": [_attribute(str(name), item) for name, item in value.items()]}
|
|
590
|
+
}
|
|
591
|
+
elif isinstance(value, list):
|
|
592
|
+
encoded = {"arrayValue": {"values": [_attribute("", item)["value"] for item in value]}}
|
|
593
|
+
elif isinstance(value, bool):
|
|
594
|
+
encoded = {"boolValue": value}
|
|
595
|
+
elif isinstance(value, int):
|
|
596
|
+
encoded = {"intValue": str(value)}
|
|
597
|
+
elif isinstance(value, float):
|
|
598
|
+
encoded = {"doubleValue": value}
|
|
599
|
+
else:
|
|
600
|
+
encoded = {"stringValue": str(value)}
|
|
601
|
+
return {"key": key, "value": encoded}
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _nonnegative_timing(value: Any, field: str) -> float:
|
|
605
|
+
if (
|
|
606
|
+
isinstance(value, bool)
|
|
607
|
+
or not isinstance(value, int | float)
|
|
608
|
+
or not math.isfinite(value)
|
|
609
|
+
or value < 0
|
|
610
|
+
):
|
|
611
|
+
raise TraceValidationError(field, "must be a finite nonnegative number")
|
|
612
|
+
return float(value)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _duration_ms(started_ns: int | None, ended_ns: int | None, span: dict[str, Any]) -> float:
|
|
616
|
+
if started_ns is not None and ended_ns is not None and ended_ns >= started_ns:
|
|
617
|
+
return (ended_ns - started_ns) / _NANOSECONDS_PER_MILLISECOND
|
|
618
|
+
return float(span.get("duration_ms") or span.get("durationMs") or 0.0)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _trace_bounds_ns(spans: list[dict[str, Any]]) -> tuple[int, int] | None:
|
|
622
|
+
if not spans:
|
|
623
|
+
return None
|
|
624
|
+
bounds: list[tuple[int, int]] = []
|
|
625
|
+
for span in spans:
|
|
626
|
+
started_ns = _int_or_none(span.get("startTimeUnixNano") or span.get("start_time_unix_nano"))
|
|
627
|
+
ended_ns = _int_or_none(span.get("endTimeUnixNano") or span.get("end_time_unix_nano"))
|
|
628
|
+
if started_ns is None or ended_ns is None or ended_ns < started_ns:
|
|
629
|
+
return None
|
|
630
|
+
bounds.append((started_ns, ended_ns))
|
|
631
|
+
return min(started for started, _ in bounds), max(ended for _, ended in bounds)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _iso_from_ns(value: int | None) -> str:
|
|
635
|
+
if value is None:
|
|
636
|
+
return datetime.now(timezone.utc).isoformat()
|
|
637
|
+
return datetime.fromtimestamp(value / 1_000_000_000, tz=timezone.utc).isoformat()
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _ns_from_iso(value: str) -> int:
|
|
641
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
642
|
+
return int(parsed.timestamp() * 1_000_000_000)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _int_or_none(value: Any) -> int | None:
|
|
646
|
+
if value is None or value == "":
|
|
647
|
+
return None
|
|
648
|
+
return int(value)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _first_string(attrs: dict[str, Any], *keys: str) -> str | None:
|
|
652
|
+
for key in keys:
|
|
653
|
+
value = attrs.get(key)
|
|
654
|
+
if value:
|
|
655
|
+
return str(value)
|
|
656
|
+
return None
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _status(span: dict[str, Any]) -> str:
|
|
660
|
+
status = span.get("status", {})
|
|
661
|
+
if isinstance(status, dict) and (
|
|
662
|
+
status.get("code") in (2, "2") or str(status.get("code", "")).endswith("ERROR")
|
|
663
|
+
):
|
|
664
|
+
return "error"
|
|
665
|
+
return "ok"
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _error(span: dict[str, Any]) -> str | None:
|
|
669
|
+
status = span.get("status", {})
|
|
670
|
+
if isinstance(status, dict):
|
|
671
|
+
if status.get("message"):
|
|
672
|
+
return str(status["message"])
|
|
673
|
+
for event in span.get("events", []):
|
|
674
|
+
if event.get("name") == "exception":
|
|
675
|
+
return _first_string(_attributes(event), "exception.message", "exception.type")
|
|
676
|
+
return None
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
_USAGE_ATTRIBUTE_KEYS = INPUT_USAGE + OUTPUT_USAGE
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _token_provenance(attrs: dict[str, Any]) -> str | None:
|
|
683
|
+
"""Resolve a span's token provenance on import.
|
|
684
|
+
|
|
685
|
+
An AgentLoop-produced span carries its native provenance verbatim, so a round
|
|
686
|
+
trip preserves it. A third-party span does not, and is classified from what
|
|
687
|
+
it actually carried: standard ``gen_ai``/``llm`` usage attributes are emitted
|
|
688
|
+
by the instrumented SDK from provider usage, so they count as
|
|
689
|
+
``provider``-reported; a span with no usage attributes has no counts to
|
|
690
|
+
report, which is ``unavailable`` rather than a measured zero.
|
|
691
|
+
"""
|
|
692
|
+
|
|
693
|
+
native = attrs.get("agentloop.token_provenance")
|
|
694
|
+
if "agentloop.token_provenance" in attrs and (native is None or isinstance(native, str)):
|
|
695
|
+
return native
|
|
696
|
+
return PROVIDER if any(key in attrs for key in _USAGE_ATTRIBUTE_KEYS) else UNAVAILABLE
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _direct_attribute_keys() -> set[str]:
|
|
700
|
+
return {
|
|
701
|
+
"agentloop.model",
|
|
702
|
+
"agentloop.input_tokens",
|
|
703
|
+
"agentloop.output_tokens",
|
|
704
|
+
"agentloop.event_type",
|
|
705
|
+
"agentloop.operation_kind",
|
|
706
|
+
"agentloop.name",
|
|
707
|
+
"agentloop.duration_ms",
|
|
708
|
+
"agentloop.token_provenance",
|
|
709
|
+
"agentloop.trace.name",
|
|
710
|
+
"agentloop.trace.metadata",
|
|
711
|
+
"gen_ai.operation.name",
|
|
712
|
+
"gen_ai.request.model",
|
|
713
|
+
"gen_ai.response.model",
|
|
714
|
+
"gen_ai.tool.name",
|
|
715
|
+
}
|