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.
Files changed (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. dashboard/value_view.py +105 -0
@@ -0,0 +1,496 @@
1
+ """Structural OTLP bounds and payload minimization before the existing parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from copy import deepcopy
7
+ from hashlib import sha256
8
+ from typing import Any
9
+
10
+ from agentloop.interoperability.validation import ImportLimits, ImportValidationError
11
+ from agentloop.interventions import canonical_json
12
+ from agentloop.otel import _attributes
13
+ from agentloop.otel_semantics import INPUT_USAGE, OUTPUT_USAGE
14
+
15
+ _STRUCTURAL_STRINGS = frozenset(
16
+ {
17
+ "gen_ai.operation.name",
18
+ "gen_ai.provider.name",
19
+ "gen_ai.system",
20
+ "gen_ai.request.model",
21
+ "gen_ai.response.model",
22
+ "gen_ai.conversation.id",
23
+ "gen_ai.tool.name",
24
+ "gen_ai.tool.call.id",
25
+ "gen_ai.agent.name",
26
+ "openinference.span.kind",
27
+ "service.name",
28
+ "service.version",
29
+ "service.instance.id",
30
+ "telemetry.sdk.name",
31
+ "telemetry.sdk.version",
32
+ "telemetry.sdk.language",
33
+ "agent.name",
34
+ "agent.version",
35
+ "agent.id",
36
+ "agent.role",
37
+ "parent.agent.id",
38
+ "parent.agent.name",
39
+ "parent.session.id",
40
+ "parent_span_id",
41
+ "request.id",
42
+ "turn.id",
43
+ "harness.name",
44
+ "harness.type",
45
+ "harness.version",
46
+ "runtime.harness",
47
+ "runtime.version",
48
+ "omnigent.harness",
49
+ "omnigent.harness.version",
50
+ "omnigent.integration_mode",
51
+ "omnigent.agent.id",
52
+ "omnigent.agent.role",
53
+ "omnigent.parent_agent.id",
54
+ "omnigent.parent_session.id",
55
+ "omnigent.request.id",
56
+ "omnigent.turn.id",
57
+ "omnigent.skill.name",
58
+ "omnigent.span.role",
59
+ "policy.name",
60
+ "policy.phase",
61
+ "policy.action",
62
+ "policy.tool_call_id",
63
+ "policy.dispatch_phase",
64
+ "approval.id",
65
+ "approval.resolution",
66
+ "relationship",
67
+ "session.id",
68
+ "trajectory.id",
69
+ "atif.schema_version",
70
+ "llm.model_name",
71
+ "llm.provider",
72
+ "llm.response.model_name",
73
+ "llm.request.model_name",
74
+ "embedding.model_name",
75
+ "tool.name",
76
+ "tool.id",
77
+ "tool_call.id",
78
+ "mcp.method.name",
79
+ "mcp.session.id",
80
+ "rpc.system",
81
+ "http.request.method",
82
+ "http.method",
83
+ "error.type",
84
+ "exception.type",
85
+ "gen_ai.evaluation.name",
86
+ "gen_ai.evaluation.score.label",
87
+ "evaluation.name",
88
+ "evaluation.label",
89
+ "annotation.name",
90
+ "annotation.label",
91
+ "agentloop.event_type",
92
+ "agentloop.operation_kind",
93
+ "agentloop.model",
94
+ "agentloop.name",
95
+ "agentloop.run_id",
96
+ "agentloop.native_event_id",
97
+ "agentloop.native_parent_id",
98
+ "agentloop.token_provenance",
99
+ "agentloop.trace.name",
100
+ "agentloop.trace.started_at",
101
+ "agentloop.trace.ended_at",
102
+ }
103
+ )
104
+
105
+
106
+ def fail(field: str, reason: str, code: str = "invalid_otlp") -> None:
107
+ raise ImportValidationError(code, field, reason)
108
+
109
+
110
+ def object_value(value: Any, field: str) -> dict:
111
+ if not isinstance(value, dict):
112
+ fail(field, "must be an object")
113
+ return value
114
+
115
+
116
+ def array(value: Any, field: str) -> list:
117
+ if not isinstance(value, list):
118
+ fail(field, "must be an array")
119
+ return value
120
+
121
+
122
+ def wire_id(value: Any, field: str, length: int, *, missing: bool = False) -> str | None:
123
+ if missing and value in (None, ""):
124
+ return None
125
+ if (
126
+ not isinstance(value, str)
127
+ or not re.fullmatch(r"[0-9a-fA-F]{" + str(length) + "}", value)
128
+ or int(value, 16) == 0
129
+ ):
130
+ fail(field, "must be a nonzero hexadecimal OTLP identifier")
131
+ return value.lower()
132
+
133
+
134
+ def attributes(value: Any, field: str) -> dict:
135
+ """Validate the standard list form and the existing parser's object form."""
136
+ if value is None:
137
+ return {}
138
+ if isinstance(value, dict):
139
+ if any(not key or len(key.encode()) > 512 for key in value):
140
+ fail(field, "attribute keys must be bounded and nonempty")
141
+ return _attributes({"attributes": value})
142
+ items = array(value, field)
143
+ seen = set()
144
+ for item in items:
145
+ object_value(item, field)
146
+ key = item.get("key")
147
+ if not isinstance(key, str) or not key or len(key.encode()) > 512 or key in seen:
148
+ fail(field, "attribute keys must be bounded and unique")
149
+ seen.add(key)
150
+ _any_value(item.get("value"), field + ".value")
151
+ return _attributes({"attributes": items})
152
+
153
+
154
+ def _any_value(value: Any, field: str) -> None:
155
+ value = object_value(value, field)
156
+ if not value:
157
+ return
158
+ kinds = {
159
+ "stringValue",
160
+ "boolValue",
161
+ "intValue",
162
+ "doubleValue",
163
+ "bytesValue",
164
+ "arrayValue",
165
+ "kvlistValue",
166
+ }
167
+ if len(value) != 1 or not set(value) <= kinds:
168
+ fail(field, "unsupported or ambiguous AnyValue")
169
+ key, item = next(iter(value.items()))
170
+ if key in {"stringValue", "bytesValue"} and not isinstance(item, str):
171
+ fail(field, "string/bytes attributes must be strings")
172
+ if key == "boolValue" and type(item) is not bool:
173
+ fail(field, "boolean attributes must be boolean")
174
+ if key == "doubleValue" and type(item) not in {int, float}:
175
+ fail(field, "numeric attributes must be numbers")
176
+ if key == "arrayValue":
177
+ for child in array(object_value(item, field).get("values", []), field):
178
+ _any_value(child, field)
179
+ if key == "kvlistValue":
180
+ attributes(object_value(item, field).get("values", []), field)
181
+
182
+
183
+ def minimized_value(value: Any, key: str, *, capture_content: bool) -> Any:
184
+ """Retain structural/numeric attributes; content opt-in never captures secrets."""
185
+ key = key.removeprefix("agentloop.metadata.")
186
+ compact = key.lower().replace("_", "").replace("-", "")
187
+ sensitive = any(
188
+ word in compact
189
+ for word in (
190
+ "password",
191
+ "passwd",
192
+ "authorization",
193
+ "apikey",
194
+ "secret",
195
+ "credential",
196
+ "bearer",
197
+ "cookie",
198
+ "privatekey",
199
+ )
200
+ ) or (
201
+ "token" in compact
202
+ and not any(
203
+ word in compact
204
+ for word in (
205
+ "tokencount",
206
+ "usage",
207
+ "inputtokens",
208
+ "outputtokens",
209
+ "prompttokens",
210
+ "completiontokens",
211
+ "cachedtokens",
212
+ )
213
+ )
214
+ )
215
+ private = sensitive or any(
216
+ word in compact
217
+ for word in (
218
+ "reasoningcontent",
219
+ "chainofthought",
220
+ "tokenids",
221
+ "logprobs",
222
+ "exceptionmessage",
223
+ "exceptionstacktrace",
224
+ )
225
+ )
226
+ if private or isinstance(value, str) and not (key in _STRUCTURAL_STRINGS or capture_content):
227
+ encoded = canonical_json(value).encode()
228
+ return {
229
+ "capture": "omitted",
230
+ "sha256": sha256(encoded).hexdigest(),
231
+ "size_bytes": len(encoded),
232
+ }
233
+ if isinstance(value, dict):
234
+ return {
235
+ name: minimized_value(item, name, capture_content=capture_content)
236
+ for name, item in value.items()
237
+ }
238
+ if isinstance(value, list):
239
+ return [minimized_value(item, key, capture_content=capture_content) for item in value]
240
+ return deepcopy(value)
241
+
242
+
243
+ def minimized_attributes(owner: dict, *, capture_content: bool) -> None:
244
+ raw = owner.get("attributes")
245
+ decoded = attributes(raw, "attributes")
246
+ clean = {
247
+ key: minimized_value(value, key, capture_content=capture_content)
248
+ for key, value in decoded.items()
249
+ }
250
+ # Attribute object form is already supported by the core parser and avoids
251
+ # reimplementing protobuf AnyValue serialization.
252
+ if raw is not None:
253
+ owner["attributes"] = clean
254
+
255
+
256
+ def flatten(
257
+ payload: Any, limits: ImportLimits, *, capture_content: bool
258
+ ) -> tuple[dict, list[dict], list[dict]]:
259
+ """Return a validated/minimized payload plus source span and log witnesses."""
260
+ object_value(payload, "record")
261
+ _numeric_string_bounds(payload, limits)
262
+ if not ("resourceSpans" in payload or "resourceLogs" in payload or "spans" in payload):
263
+ fail("record", "OTLP resource spans/logs or a spans array are required")
264
+ clean = deepcopy(payload)
265
+ spans, logs = [], []
266
+ root_omissions = len(set(payload) - {"resourceSpans", "resourceLogs", "spans", "resource"})
267
+ resources = clean.get("resourceSpans", [])
268
+ if "spans" in clean:
269
+ resources = [
270
+ {"resource": clean.get("resource", {}), "scopeSpans": [{"spans": clean["spans"]}]}
271
+ ]
272
+ clean = {"resourceSpans": resources, "resourceLogs": clean.get("resourceLogs", [])}
273
+ for field, scope_field, items_field, destination in (
274
+ ("resourceSpans", "scopeSpans", "spans", spans),
275
+ ("resourceLogs", "scopeLogs", "logRecords", logs),
276
+ ):
277
+ blocks = resources if field == "resourceSpans" else clean.get(field, [])
278
+ for resource in array(blocks, field):
279
+ object_value(resource, field)
280
+ context = deepcopy(resource.get("resource", {}))
281
+ object_value(context, "resource")
282
+ attributes(context.get("attributes"), "resource.attributes")
283
+ if "resource" in resource:
284
+ minimized_attributes(resource["resource"], capture_content=capture_content)
285
+ for scope in array(resource.get(scope_field, []), scope_field):
286
+ object_value(scope, scope_field)
287
+ scope_info = object_value(scope.get("scope", {}), "scope")
288
+ attributes(scope_info.get("attributes"), "scope.attributes")
289
+ source_scope = deepcopy(scope_info)
290
+ minimized_attributes(scope_info, capture_content=capture_content)
291
+ for key in list(scope_info):
292
+ if key not in {"name", "version", "attributes"}:
293
+ scope_info[key] = minimized_value(
294
+ scope_info[key], key, capture_content=capture_content
295
+ )
296
+ for ordinal, item in enumerate(array(scope.get(items_field, []), items_field)):
297
+ object_value(item, items_field)
298
+ source = deepcopy(item)
299
+ for canonical, alias in (
300
+ ("traceId", "trace_id"),
301
+ ("spanId", "span_id"),
302
+ ("parentSpanId", "parent_span_id"),
303
+ ("startTimeUnixNano", "start_time_unix_nano"),
304
+ ("endTimeUnixNano", "end_time_unix_nano"),
305
+ ):
306
+ if alias in item:
307
+ if canonical in item and item[canonical] != item[alias]:
308
+ fail(canonical, "contradictory source field aliases")
309
+ item[canonical] = item[alias]
310
+ if items_field == "spans":
311
+ for field_name in ("startTimeUnixNano", "endTimeUnixNano"):
312
+ value = item.get(field_name)
313
+ if value is not None:
314
+ if type(value) not in {int, str}:
315
+ fail(
316
+ field_name, "timestamp must be an unsigned decimal integer"
317
+ )
318
+ try:
319
+ number = int(value)
320
+ except ValueError:
321
+ fail(
322
+ field_name, "timestamp must be an unsigned decimal integer"
323
+ )
324
+ if number < 0:
325
+ fail(
326
+ field_name, "timestamp must be an unsigned decimal integer"
327
+ )
328
+ item[field_name] = str(number)
329
+ trace_id = wire_id(item.get("traceId"), "traceId", 32, missing=True)
330
+ span_id = wire_id(item.get("spanId"), "spanId", 16, missing=True)
331
+ if items_field == "spans":
332
+ wire_id(item.get("parentSpanId"), "parentSpanId", 16, missing=True)
333
+ for link in array(item.get("links", []), "links"):
334
+ object_value(link, "links")
335
+ wire_id(link.get("traceId"), "links.traceId", 32)
336
+ wire_id(link.get("spanId"), "links.spanId", 16)
337
+ minimized_attributes(link, capture_content=capture_content)
338
+ for event in array(item.get("events", []), "events"):
339
+ object_value(event, "events")
340
+ minimized_attributes(event, capture_content=capture_content)
341
+ for key in list(event):
342
+ if key not in {
343
+ "name",
344
+ "timeUnixNano",
345
+ "attributes",
346
+ "droppedAttributesCount",
347
+ }:
348
+ event[key] = minimized_value(
349
+ event[key], key, capture_content=capture_content
350
+ )
351
+ if "status" in item:
352
+ status = object_value(item["status"], "status")
353
+ code = status.get("code")
354
+ if code is not None and (
355
+ type(code) not in {int, str}
356
+ or code
357
+ not in {
358
+ 0,
359
+ 1,
360
+ 2,
361
+ "STATUS_CODE_UNSET",
362
+ "STATUS_CODE_OK",
363
+ "STATUS_CODE_ERROR",
364
+ }
365
+ ):
366
+ fail("status.code", "unsupported status code")
367
+ status.pop("message", None)
368
+ else:
369
+ if "body" in item:
370
+ _any_value(item["body"], "body")
371
+ item["body"] = (
372
+ {"stringValue": "[omitted]"}
373
+ if not capture_content
374
+ else item["body"]
375
+ )
376
+ original_attrs = attributes(source.get("attributes"), "attributes")
377
+ minimized_attributes(item, capture_content=capture_content)
378
+ if len(canonical_json(item).encode()) > limits.max_metadata_bytes:
379
+ fail(
380
+ "span.metadata",
381
+ "metadata exceeds configured bounds",
382
+ "record_limit_exceeded",
383
+ )
384
+ destination.append(
385
+ {
386
+ "span": item,
387
+ "trace_id": trace_id,
388
+ "span_id": span_id,
389
+ "source_sha256": sha256(
390
+ canonical_json(
391
+ {
392
+ "item": source,
393
+ "resource": context,
394
+ "scope": source_scope,
395
+ "schema_url": scope.get(
396
+ "schemaUrl", resource.get("schemaUrl")
397
+ ),
398
+ }
399
+ ).encode()
400
+ ).hexdigest(),
401
+ "attributes": original_attrs,
402
+ "resource": attributes(
403
+ context.get("attributes"), "resource.attributes"
404
+ ),
405
+ "scope": source_scope,
406
+ "ordinal": ordinal,
407
+ "unrepresented_fields": root_omissions
408
+ + len(
409
+ set(resource) - {"resource", "scopeSpans", "scopeLogs", "schemaUrl"}
410
+ )
411
+ + (
412
+ len(
413
+ set(source)
414
+ - {
415
+ "traceId",
416
+ "spanId",
417
+ "parentSpanId",
418
+ "trace_id",
419
+ "span_id",
420
+ "parent_span_id",
421
+ "startTimeUnixNano",
422
+ "endTimeUnixNano",
423
+ "start_time_unix_nano",
424
+ "end_time_unix_nano",
425
+ "name",
426
+ "kind",
427
+ "traceState",
428
+ "flags",
429
+ "status",
430
+ "attributes",
431
+ "events",
432
+ "links",
433
+ "droppedAttributesCount",
434
+ "droppedEventsCount",
435
+ "droppedLinksCount",
436
+ }
437
+ )
438
+ if items_field == "spans"
439
+ else 0
440
+ ),
441
+ }
442
+ )
443
+ return clean, spans, logs
444
+
445
+
446
+ def _numeric_string_bounds(payload: dict, limits: ImportLimits) -> None:
447
+ """Bound decimal strings before the core parser calls int on wire values."""
448
+ numeric_keys = {
449
+ "intValue",
450
+ "startTimeUnixNano",
451
+ "endTimeUnixNano",
452
+ "timeUnixNano",
453
+ "observedTimeUnixNano",
454
+ "start_time_unix_nano",
455
+ "end_time_unix_nano",
456
+ "agentloop.input_tokens",
457
+ "agentloop.output_tokens",
458
+ "gen_ai.usage.cache_read.input_tokens",
459
+ "gen_ai.usage.cache_write.input_tokens",
460
+ "llm.token_count.prompt_details.cache_read",
461
+ "llm.token_count.prompt_details.cache_write",
462
+ "gen_ai.usage.reasoning.output_tokens",
463
+ "llm.token_count.completion_details.reasoning",
464
+ "llm.token_count.total",
465
+ *INPUT_USAGE,
466
+ *OUTPUT_USAGE,
467
+ }
468
+ pending = [payload]
469
+ while pending:
470
+ node = pending.pop()
471
+ if isinstance(node, dict):
472
+ for key, value in node.items():
473
+ if (
474
+ key in numeric_keys
475
+ and isinstance(value, str)
476
+ and len(value) > limits.max_number_chars
477
+ ):
478
+ fail(
479
+ "numeric_string",
480
+ "numeric representation exceeds configured bounds",
481
+ "record_limit_exceeded",
482
+ )
483
+ if isinstance(node.get("key"), str) and node["key"] in numeric_keys:
484
+ encoded = node.get("value")
485
+ if isinstance(encoded, dict) and any(
486
+ isinstance(value, str) and len(value) > limits.max_number_chars
487
+ for value in encoded.values()
488
+ ):
489
+ fail(
490
+ "numeric_string",
491
+ "numeric representation exceeds configured bounds",
492
+ "record_limit_exceeded",
493
+ )
494
+ pending.extend(value for value in node.values() if isinstance(value, (dict, list)))
495
+ elif isinstance(node, list):
496
+ pending.extend(value for value in node if isinstance(value, (dict, list)))