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,274 @@
1
+ """Operational guardrails for declared retries and repeated caller state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ from collections.abc import Iterable
7
+ from dataclasses import asdict, dataclass, field
8
+ from typing import Any
9
+
10
+ from agentloop.budget_types import RETRY_SOURCES, count
11
+ from agentloop.harness import BOUNDARIES, Decision, Hook, HookContext, Policy
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class LoopLimits:
16
+ """Retry caps and optional bounded-history repetition/oscillation limits."""
17
+
18
+ max_retries_per_step: int | None = 3
19
+ max_identical_calls: int | None = None
20
+ oscillation_period: int | None = None
21
+ oscillation_repeats: int = 3
22
+
23
+ def __post_init__(self) -> None:
24
+ count(self.max_retries_per_step, "max_retries_per_step")
25
+ count(self.max_identical_calls, "max_identical_calls")
26
+ if self.max_identical_calls == 0:
27
+ raise ValueError("max_identical_calls must allow at least one call")
28
+ if self.max_identical_calls is not None and self.max_identical_calls > 256:
29
+ raise ValueError("identical-call history is bounded to 256 calls")
30
+ if self.oscillation_period is not None and (
31
+ type(self.oscillation_period) is not int or not 2 <= self.oscillation_period <= 32
32
+ ):
33
+ raise ValueError("oscillation_period must be between 2 and 32")
34
+ if type(self.oscillation_repeats) is not int or not 2 <= self.oscillation_repeats <= 8:
35
+ raise ValueError("oscillation_repeats must be between 2 and 8")
36
+
37
+
38
+ @dataclass
39
+ class _StepState:
40
+ retries: int = 0
41
+ retries_in_flight: int = 0
42
+ retry_sources: dict[str, int] = field(default_factory=lambda: dict.fromkeys(RETRY_SOURCES, 0))
43
+ history: deque = field(default_factory=deque)
44
+ pending: dict[str, Any] = field(default_factory=dict)
45
+ last_status: str | None = None
46
+ last_decision_id: str | None = None
47
+ last_mutating: bool | None = None
48
+ last_arguments: str | None = None
49
+ active_calls: int = 0
50
+ mutating_in_flight: int = 0
51
+ semantic_epoch: int = 0
52
+
53
+
54
+ class _LoopGuard:
55
+ def __init__(self, limits: LoopLimits) -> None:
56
+ self.limits = limits
57
+ self.window = max(
58
+ limits.max_identical_calls or 1,
59
+ (limits.oscillation_period or 1) * limits.oscillation_repeats,
60
+ )
61
+
62
+ def _feedback(
63
+ self,
64
+ context: HookContext,
65
+ state: _StepState,
66
+ action: str,
67
+ reason: str,
68
+ message: str,
69
+ retry_of: str | None = None,
70
+ ) -> Decision:
71
+ step = context.dispatch.step
72
+ refs = []
73
+ if step is not None:
74
+ refs.append(f"step:{step.step_id}")
75
+ if step.argument_fingerprint is not None:
76
+ refs.append(f"arguments:sha256:{step.argument_fingerprint}")
77
+ if step.progress_fingerprint is not None:
78
+ refs.append(f"progress:sha256:{step.progress_fingerprint}")
79
+ if context.dispatch.retry_source is not None:
80
+ refs.append(f"retry_source:{context.dispatch.retry_source}")
81
+ return Decision(
82
+ action, reason, evidence_refs=tuple(refs), retry_of=retry_of, feedback=message
83
+ )
84
+
85
+ def _oscillating(self, history: list[tuple[str, str]]) -> bool:
86
+ maximum = self.limits.oscillation_period
87
+ if maximum is None:
88
+ return False
89
+ for period in range(2, maximum + 1):
90
+ size = period * self.limits.oscillation_repeats
91
+ if len(history) < size:
92
+ continue
93
+ tail = history[-size:]
94
+ pattern = tail[:period]
95
+ if len(set(pattern)) > 1 and all(
96
+ tail[index] == pattern[index % period] for index in range(size)
97
+ ):
98
+ return True
99
+ return False
100
+
101
+ def __call__(self, context: HookContext) -> Decision:
102
+ step = context.dispatch.step
103
+ if step is None:
104
+ if context.dispatch.retry_source is not None:
105
+ return Decision(
106
+ "escalate",
107
+ "retry_step_unknown",
108
+ feedback="Retry admission needs an explicit step identity and declared safe semantics.",
109
+ )
110
+ return Decision(
111
+ "continue",
112
+ "loop_evidence_unavailable",
113
+ feedback="No step identity was supplied; no semantic no-progress conclusion is available.",
114
+ )
115
+ states = context.state.setdefault("steps", {})
116
+ key = (context.branch_id, step.step_id)
117
+ state = states.get(key)
118
+ if state is None:
119
+ state = states[key] = _StepState(history=deque(maxlen=self.window))
120
+ retry = context.dispatch.retry_source is not None
121
+ if context.hook.phase == "after":
122
+ pending = state.pending.pop(context.call_id, None)
123
+ if pending is not None:
124
+ if pending["admitted"]:
125
+ state.active_calls -= 1
126
+ if pending["mutating"] is True:
127
+ state.mutating_in_flight -= 1
128
+ if pending["retry_reserved"]:
129
+ state.retries_in_flight -= 1
130
+ if context.dispatched:
131
+ if pending["overlapped"] or pending["reset_history"]:
132
+ state.history.clear()
133
+ state.semantic_epoch += 1
134
+ if pending["retry"]:
135
+ state.retries += 1
136
+ state.retry_sources[pending["retry_source"]] += 1
137
+ if pending["history"] is not None and pending["epoch"] == state.semantic_epoch:
138
+ state.history.append(pending["history"])
139
+ state.last_status = context.status
140
+ state.last_decision_id = pending["decision_id"]
141
+ state.last_mutating = step.mutating
142
+ state.last_arguments = step.argument_fingerprint
143
+ return self._feedback(
144
+ context,
145
+ state,
146
+ "continue",
147
+ "loop_attempt_recorded",
148
+ "Attempt outcome recorded; retries remain host-controlled.",
149
+ )
150
+
151
+ prior = state.last_decision_id if retry else None
152
+ action, reason = "continue", "loop_continue"
153
+ message = "The declared step is within its configured operational limits."
154
+ evidence = None
155
+ reset_history = retry or step.polling
156
+ overlapped = state.active_calls > 0
157
+ retry_count = state.retries + state.retries_in_flight
158
+ ambiguous_repeat = (
159
+ not retry
160
+ and state.last_status in {"error", "cancelled", "closed"}
161
+ and step.argument_fingerprint is not None
162
+ and step.argument_fingerprint == state.last_arguments
163
+ and (step.mutating is not False or state.last_mutating is not False)
164
+ and not step.retry_safe
165
+ )
166
+ if ambiguous_repeat:
167
+ action, reason = "escalate", "ambiguous_side_effect_repeat"
168
+ message = "Identical input follows an ambiguous potentially mutating attempt; explicit safe retry semantics are required."
169
+ elif retry:
170
+ mutating_in_flight = state.mutating_in_flight > 0
171
+ safe = step.retry_safe or (
172
+ step.mutating is False
173
+ and state.last_mutating is not True
174
+ and not mutating_in_flight
175
+ )
176
+ if not safe:
177
+ action, reason = "escalate", "retry_safety_unknown"
178
+ message = "Retrying a potentially mutating step requires declared safe retry semantics; prior side effects may be ambiguous."
179
+ elif (
180
+ self.limits.max_retries_per_step is not None
181
+ and retry_count >= self.limits.max_retries_per_step
182
+ ):
183
+ action, reason = "stop", "step_retry_limit"
184
+ message = "The step reached its configured retry limit; no additional attempt is admitted."
185
+ else:
186
+ message = f"Declared {context.dispatch.retry_source} retry admitted within its bound; no-progress heuristics are not applied to retries."
187
+ elif step.polling:
188
+ reason = "declared_polling"
189
+ message = (
190
+ "Declared polling bypasses semantic repetition guards; global budgets still apply."
191
+ )
192
+ elif step.argument_fingerprint is None or step.progress_fingerprint is None:
193
+ reset_history = True
194
+ reason = "loop_evidence_unavailable"
195
+ message = "Input/progress evidence is incomplete; repeated names alone do not establish a no-progress loop."
196
+ else:
197
+ evidence = (step.argument_fingerprint, step.progress_fingerprint)
198
+ if overlapped:
199
+ evidence = None
200
+ reset_history = True
201
+ reason = "loop_concurrent_evidence"
202
+ message = "Overlapping attempts do not establish a sequential no-progress cycle; global budgets still apply."
203
+ history = list(state.history)
204
+ repeated = 0
205
+ for previous in reversed(history):
206
+ if previous != evidence:
207
+ break
208
+ repeated += 1
209
+ if (
210
+ evidence is not None
211
+ and self.limits.max_identical_calls is not None
212
+ and repeated >= self.limits.max_identical_calls
213
+ ):
214
+ action, reason = "stop", "identical_step_limit"
215
+ message = f"Unchanged input/progress fingerprints reached the configured bound ({self.limits.max_identical_calls}); this is not proof of semantic uselessness."
216
+ elif evidence is not None and self._oscillating(history + [evidence]):
217
+ action, reason = "stop", "oscillation_limit"
218
+ message = "Caller-declared input/progress fingerprints repeat a configured cycle; validate the task-specific meaning before changing the workflow."
219
+ admitted = action == "continue" or context.mode == "shadow"
220
+ retry_reserved = retry and admitted
221
+ if retry_reserved:
222
+ state.retries_in_flight += 1
223
+ if admitted:
224
+ state.active_calls += 1
225
+ if step.mutating is True:
226
+ state.mutating_in_flight += 1
227
+ state.pending[context.call_id] = {
228
+ "retry": retry,
229
+ "retry_source": context.dispatch.retry_source,
230
+ "retry_reserved": retry_reserved,
231
+ "history": evidence if admitted else None,
232
+ "decision_id": context.decision_id,
233
+ "admitted": admitted,
234
+ "reset_history": reset_history,
235
+ "overlapped": overlapped,
236
+ "mutating": step.mutating,
237
+ "epoch": state.semantic_epoch,
238
+ }
239
+ return self._feedback(context, state, action, reason, message, retry_of=prior)
240
+
241
+
242
+ def loop_guard_policy(
243
+ limits: LoopLimits = LoopLimits(),
244
+ *,
245
+ policy_id: str = "loop_guard",
246
+ priority: int = 200,
247
+ boundaries: Iterable[str] = BOUNDARIES,
248
+ ) -> Policy:
249
+ """Compose with budget_policy for total iteration/retry/resource admission."""
250
+ if type(limits) is not LoopLimits:
251
+ raise ValueError("limits must be LoopLimits")
252
+ try:
253
+ selected = frozenset(boundaries)
254
+ except TypeError:
255
+ raise ValueError("loop boundaries must be named hooks") from None
256
+ if not selected or not selected <= BOUNDARIES:
257
+ raise ValueError("unsupported loop boundary")
258
+ return Policy(
259
+ policy_id,
260
+ "1.0",
261
+ _LoopGuard(limits),
262
+ hooks=frozenset(
263
+ Hook(boundary, phase) for boundary in selected for phase in ("before", "after")
264
+ ),
265
+ actions=frozenset({"continue", "stop", "escalate"}),
266
+ priority=priority,
267
+ configuration={
268
+ "limits": asdict(limits),
269
+ "automatic_retries": False,
270
+ "semantic_progress_source": "caller_declared",
271
+ "history_bounded": True,
272
+ "boundaries": sorted(selected),
273
+ },
274
+ )
@@ -0,0 +1,66 @@
1
+ """Explicit, payload-free evidence for operational retry and loop guards."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import re
8
+ from dataclasses import dataclass
9
+ from hashlib import sha256
10
+ from typing import Any
11
+
12
+ _DIGEST = re.compile(r"[a-f0-9]{64}\Z")
13
+
14
+
15
+ def fingerprint(value: Any) -> str:
16
+ """Hash selected finite JSON values; never serialize arbitrary object reprs.
17
+
18
+ Mapping order is ignored. Represent meaningful order as a list. Inputs are
19
+ not retained, but hashes of predictable or sensitive values are not anonymous.
20
+ """
21
+
22
+ def validate(item: Any) -> None:
23
+ if item is None or type(item) in {str, bool, int}:
24
+ return
25
+ if type(item) is float and math.isfinite(item):
26
+ return
27
+ if type(item) is list:
28
+ for child in item:
29
+ validate(child)
30
+ return
31
+ if type(item) is dict and all(type(key) is str for key in item):
32
+ for child in item.values():
33
+ validate(child)
34
+ return
35
+ raise ValueError("fingerprint inputs must be finite JSON values with string keys")
36
+
37
+ try:
38
+ validate(value)
39
+ encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
40
+ except RecursionError:
41
+ raise ValueError("fingerprint inputs must not contain cycles") from None
42
+ return sha256(encoded).hexdigest()
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class StepInfo:
47
+ """Caller-declared step identity, state evidence, polling, and retry safety."""
48
+
49
+ step_id: str
50
+ argument_fingerprint: str | None = None
51
+ progress_fingerprint: str | None = None
52
+ polling: bool = False
53
+ mutating: bool | None = None
54
+ retry_safe: bool = False
55
+
56
+ def __post_init__(self) -> None:
57
+ if not isinstance(self.step_id, str) or not self.step_id:
58
+ raise ValueError("step_id must be a nonempty opaque identifier")
59
+ for name in ("argument_fingerprint", "progress_fingerprint"):
60
+ value = getattr(self, name)
61
+ if value is not None and (not isinstance(value, str) or not _DIGEST.fullmatch(value)):
62
+ raise ValueError("step fingerprints must be SHA-256 hex digests")
63
+ if type(self.polling) is not bool or type(self.retry_safe) is not bool:
64
+ raise ValueError("polling and retry_safe must be booleans")
65
+ if self.mutating is not None and type(self.mutating) is not bool:
66
+ raise ValueError("mutating must be a boolean or unknown")
agentloop/markdown.py ADDED
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ _LINE_BREAKS = re.compile(r"\r\n?|\n|\x85|\u2028|\u2029")
7
+ _BACKTICK_RUNS = re.compile(r"`+")
8
+ _SAFE_INFO_STRING = re.compile(r"[A-Za-z0-9_.+-]*\Z")
9
+
10
+ # Markdown treats these characters as inline syntax, while HTML parsers give
11
+ # angle brackets and ampersands additional meaning. Character references keep
12
+ # the rendered text unchanged without relying on escape ordering.
13
+ _INLINE_ESCAPES = {
14
+ "&": "&amp;",
15
+ "<": "&lt;",
16
+ ">": "&gt;",
17
+ "\\": "&#92;",
18
+ "`": "&#96;",
19
+ "*": "&#42;",
20
+ "_": "&#95;",
21
+ "[": "&#91;",
22
+ "]": "&#93;",
23
+ "~": "&#126;",
24
+ }
25
+ _TABLE_TRANSLATION = str.maketrans({"|": "&#124;"})
26
+
27
+
28
+ def markdown_text(value: Any) -> str:
29
+ """Render an untrusted value as single-line Markdown text."""
30
+
31
+ content = _single_line(value)
32
+ return "".join(
33
+ character
34
+ if character == "_" and _underscore_is_inside_identifier(content, index)
35
+ else _INLINE_ESCAPES.get(character, character)
36
+ for index, character in enumerate(content)
37
+ )
38
+
39
+
40
+ def markdown_heading(value: Any) -> str:
41
+ """Render an untrusted value inside a heading without allowing new blocks."""
42
+
43
+ return markdown_text(value)
44
+
45
+
46
+ def markdown_table_cell(value: Any) -> str:
47
+ """Render an untrusted value without adding Markdown table columns or rows."""
48
+
49
+ return markdown_text(value).translate(_TABLE_TRANSLATION)
50
+
51
+
52
+ def markdown_code_span(value: Any) -> str:
53
+ """Render an untrusted value in an inline code span with a safe delimiter."""
54
+
55
+ content = _single_line(value)
56
+ if not content:
57
+ return ""
58
+
59
+ delimiter = "`" * (_longest_backtick_run(content) + 1)
60
+ needs_padding = content.startswith("`") or content.endswith("`")
61
+ if content.strip(" ") and (content.startswith(" ") or content.endswith(" ")):
62
+ needs_padding = True
63
+ padding = " " if needs_padding else ""
64
+ return f"{delimiter}{padding}{content}{padding}{delimiter}"
65
+
66
+
67
+ def markdown_fenced_code(value: Any, *, language: str = "") -> str:
68
+ """Render untrusted multiline code using a fence longer than its contents."""
69
+
70
+ if not _SAFE_INFO_STRING.fullmatch(language):
71
+ raise ValueError("Markdown fence language contains unsafe characters")
72
+
73
+ content = _normalize_line_breaks(_string(value))
74
+ fence = "`" * max(3, _longest_backtick_run(content) + 1)
75
+ opening = f"{fence}{language}"
76
+ separator = "" if content.endswith("\n") else "\n"
77
+ return f"{opening}\n{content}{separator}{fence}"
78
+
79
+
80
+ def _single_line(value: Any) -> str:
81
+ return _LINE_BREAKS.sub(" ", _string(value))
82
+
83
+
84
+ def _normalize_line_breaks(value: str) -> str:
85
+ return _LINE_BREAKS.sub("\n", value)
86
+
87
+
88
+ def _longest_backtick_run(value: str) -> int:
89
+ return max((len(match.group(0)) for match in _BACKTICK_RUNS.finditer(value)), default=0)
90
+
91
+
92
+ def _underscore_is_inside_identifier(value: str, index: int) -> bool:
93
+ return (
94
+ index > 0
95
+ and index + 1 < len(value)
96
+ and value[index - 1].isalnum()
97
+ and value[index + 1].isalnum()
98
+ )
99
+
100
+
101
+ def _string(value: Any) -> str:
102
+ return "" if value is None else str(value)