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
agentloop/demo.py ADDED
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from pathlib import Path
6
+
7
+ from agentloop import trace_agent, trace_model_call, trace_retry, trace_tool_call
8
+
9
+ # Every model call below declares both token counts explicitly. These traces are
10
+ # synthetic fixtures, so their counts are declared fixture values the way a real
11
+ # integration declares provider-reported usage — not measurements of the prompt
12
+ # text. Leaving a count off would make AgentLoop fall back to a whitespace word
13
+ # estimate, which records `estimated_words` provenance and correctly turns the
14
+ # demo's cost gates indeterminate (issue #133).
15
+ SYSTEM_LONG = (
16
+ "You are a careful research agent. Use evidence, verify claims, and write concise reports. "
17
+ * 20
18
+ )
19
+ SYSTEM_SHORT = (
20
+ "You are a careful research agent. Use evidence, verify claims, and write concise reports. " * 4
21
+ )
22
+
23
+
24
+ def _demo_metadata(**values: object) -> dict[str, object]:
25
+ metadata: dict[str, object] = {"synthetic": True, "source": "agentloop_demo"}
26
+ metadata.update(values)
27
+ return metadata
28
+
29
+
30
+ def sleep_ms(ms: int) -> None:
31
+ time.sleep(ms / 1000)
32
+
33
+
34
+ def fake_search(query: str) -> list[str]:
35
+ sleep_ms(300)
36
+ return [f"source-{i}-{query}" for i in range(1, 6)]
37
+
38
+
39
+ def fake_read(source: str) -> str:
40
+ sleep_ms(250)
41
+ return f"Content from {source}. " * 30
42
+
43
+
44
+ def inspect_result(result: str) -> str:
45
+ sleep_ms(120)
46
+ return f"inspected {result}"
47
+
48
+
49
+ def run_baseline(out_dir: str | Path = "runs") -> Path:
50
+ out_path = Path(out_dir) / "research_agent_baseline.json"
51
+ with trace_agent("research_agent_baseline", metadata=_demo_metadata()) as trace:
52
+ with trace_model_call(
53
+ "plan",
54
+ model="gpt-4.1",
55
+ input_text=SYSTEM_LONG + "Plan research on agent inference runtimes.",
56
+ input_tokens=286,
57
+ output_tokens=220,
58
+ ):
59
+ sleep_ms(400)
60
+
61
+ with trace_tool_call("search_web", metadata={"query": "agent inference runtime"}):
62
+ sources = fake_search("agent inference runtime")
63
+
64
+ summaries = []
65
+ for source in sources:
66
+ with trace_tool_call("read_source", metadata={"source": source}):
67
+ content = fake_read(source)
68
+ with trace_model_call(
69
+ "summarize_source",
70
+ model="gpt-4.1",
71
+ input_text=SYSTEM_LONG + content,
72
+ input_tokens=430,
73
+ output_tokens=350,
74
+ ):
75
+ sleep_ms(350)
76
+ summaries.append("summary for " + source)
77
+
78
+ with trace_retry("parse_failed_then_retry", metadata={"reason": "invalid json"}):
79
+ sleep_ms(250)
80
+
81
+ with trace_model_call(
82
+ "verify_claims",
83
+ model="gpt-4.1",
84
+ input_text=SYSTEM_LONG + "\n".join(summaries),
85
+ input_tokens=305,
86
+ output_tokens=450,
87
+ ):
88
+ sleep_ms(450)
89
+
90
+ with trace_model_call(
91
+ "write_report",
92
+ model="gpt-4.1",
93
+ input_text=SYSTEM_LONG + "\n".join(summaries),
94
+ input_tokens=305,
95
+ output_tokens=800,
96
+ ):
97
+ sleep_ms(550)
98
+
99
+ trace.export_json(out_path)
100
+ trace.print_report()
101
+ return out_path
102
+
103
+
104
+ def run_proof_pair(out_dir: str | Path = "runs") -> tuple[Path, Path]:
105
+ """Generate a before/after trace pair for the closed-loop product demo."""
106
+
107
+ return run_proof_baseline(out_dir), run_proof_candidate(out_dir)
108
+
109
+
110
+ def run_proof_baseline(out_dir: str | Path = "runs") -> Path:
111
+ out_path = Path(out_dir) / "agentloop_proof_baseline.json"
112
+ repeated_context = SYSTEM_LONG + "Classify and summarize every source independently. " * 20
113
+ with trace_agent(
114
+ "agentloop_proof_baseline",
115
+ metadata=_demo_metadata(schema_valid=False, quality_score=0.82),
116
+ ) as trace:
117
+ with trace_model_call(
118
+ "classify_intent",
119
+ model="gpt-4.1",
120
+ input_text="Classify this simple support request.",
121
+ input_tokens=5,
122
+ output_tokens=40,
123
+ ):
124
+ sleep_ms(220)
125
+
126
+ with trace_tool_call("search_web", metadata={"query": "agent workflow profiler"}):
127
+ sources = fake_search("agent workflow profiler")[:3]
128
+
129
+ summaries = []
130
+ for source in sources:
131
+ with trace_tool_call("fake_read", metadata={"source": source}):
132
+ fake_read(source)
133
+
134
+ for index in range(15):
135
+ with trace_model_call(
136
+ "summarize_source",
137
+ model="gpt-4.1-mini",
138
+ input_text=repeated_context + f" item {index}",
139
+ input_tokens=80,
140
+ output_tokens=60,
141
+ ):
142
+ sleep_ms(12)
143
+ summaries.append(f"summary {index}")
144
+
145
+ for index in range(8):
146
+ with trace_tool_call("plan_next", metadata={"iteration": index}):
147
+ sleep_ms(60)
148
+
149
+ for name in ["search_web", "inspect_result", "search_web", "inspect_result"]:
150
+ with trace_tool_call(name):
151
+ if name == "search_web":
152
+ fake_search("same query")
153
+ else:
154
+ inspect_result("same result")
155
+
156
+ with trace_retry("invalid_json_retry", metadata={"reason": "schema_validation_failed"}):
157
+ sleep_ms(180)
158
+
159
+ large_context = repeated_context + "\n".join(summaries) + (" Evidence chunk. " * 1600)
160
+ with trace_model_call(
161
+ "final_answer",
162
+ model="gpt-4.1",
163
+ input_text=large_context,
164
+ input_tokens=4000,
165
+ output_tokens=700,
166
+ ):
167
+ sleep_ms(500)
168
+
169
+ trace.export_json(out_path)
170
+ trace.print_report()
171
+ return out_path
172
+
173
+
174
+ def run_proof_candidate(out_dir: str | Path = "runs") -> Path:
175
+ out_path = Path(out_dir) / "agentloop_proof_candidate.json"
176
+ compact_context = (
177
+ SYSTEM_SHORT + "Use cached instructions, batch summaries, and stop after convergence."
178
+ )
179
+ with trace_agent(
180
+ "agentloop_proof_candidate",
181
+ metadata=_demo_metadata(schema_valid=True, quality_score=0.94),
182
+ ) as trace:
183
+ with trace_model_call(
184
+ "classify_intent",
185
+ model="gpt-4.1-mini",
186
+ input_text="Classify this simple support request.",
187
+ input_tokens=5,
188
+ output_tokens=30,
189
+ ):
190
+ sleep_ms(90)
191
+
192
+ with trace_tool_call("search_web", metadata={"query": "agent workflow profiler"}):
193
+ sources = fake_search("agent workflow profiler")[:3]
194
+
195
+ with trace_tool_call("fake_read_parallel", metadata={"count": len(sources)}):
196
+ with ThreadPoolExecutor(max_workers=3) as pool:
197
+ contents = list(pool.map(fake_read, sources))
198
+
199
+ with trace_model_call(
200
+ "summarize_batch",
201
+ model="gpt-4.1-mini",
202
+ input_text=compact_context + "\n".join(content[:220] for content in contents),
203
+ input_tokens=136,
204
+ output_tokens=280,
205
+ ):
206
+ sleep_ms(170)
207
+
208
+ with trace_tool_call(
209
+ "plan_next", metadata={"iteration": 1, "stop_reason": "state_converged"}
210
+ ):
211
+ sleep_ms(60)
212
+
213
+ with trace_model_call(
214
+ "final_answer",
215
+ model="gpt-4.1-mini",
216
+ input_text=compact_context + " Compressed evidence with source ids.",
217
+ input_tokens=70,
218
+ output_tokens=420,
219
+ ):
220
+ sleep_ms(220)
221
+
222
+ trace.export_json(out_path)
223
+ trace.print_report()
224
+ return out_path
225
+
226
+
227
+ def run_optimized(out_dir: str | Path = "runs") -> Path:
228
+ out_path = Path(out_dir) / "research_agent_optimized.json"
229
+ with trace_agent("research_agent_optimized", metadata=_demo_metadata()) as trace:
230
+ with trace_model_call(
231
+ "plan",
232
+ model="gpt-4.1-mini",
233
+ input_text=SYSTEM_SHORT + "Plan research on agent inference runtimes.",
234
+ input_tokens=62,
235
+ output_tokens=120,
236
+ ):
237
+ sleep_ms(200)
238
+
239
+ with trace_tool_call("search_web", metadata={"query": "agent inference runtime"}):
240
+ sources = fake_search("agent inference runtime")
241
+
242
+ with trace_tool_call("read_sources_parallel", metadata={"count": len(sources)}):
243
+ with ThreadPoolExecutor(max_workers=5) as pool:
244
+ contents = list(pool.map(fake_read, sources))
245
+
246
+ compact_context = "\n".join(content[:300] for content in contents)
247
+ with trace_model_call(
248
+ "summarize_batch",
249
+ model="gpt-4.1-mini",
250
+ input_text=SYSTEM_SHORT + compact_context,
251
+ input_tokens=221,
252
+ output_tokens=500,
253
+ ):
254
+ sleep_ms(300)
255
+
256
+ with trace_model_call(
257
+ "verify_and_write",
258
+ model="gpt-4.1",
259
+ input_text=SYSTEM_SHORT + compact_context,
260
+ input_tokens=221,
261
+ output_tokens=650,
262
+ ):
263
+ sleep_ms(450)
264
+
265
+ trace.export_json(out_path)
266
+ trace.print_report()
267
+ return out_path
268
+
269
+
270
+ def run_langgraph_style(out_dir: str | Path = "runs") -> Path:
271
+ from agentloop.integrations.langgraph import trace_node
272
+
273
+ @trace_node("retrieve_sources")
274
+ def retrieve_sources(state: dict) -> dict:
275
+ sleep_ms(250)
276
+ state["sources"] = ["paper-a", "paper-b", "paper-c"]
277
+ return state
278
+
279
+ @trace_node("rank_sources")
280
+ def rank_sources(state: dict) -> dict:
281
+ sleep_ms(180)
282
+ state["sources"] = state["sources"][:2]
283
+ return state
284
+
285
+ out_path = Path(out_dir) / "langgraph_style_demo.json"
286
+ with trace_agent("langgraph_style_demo", metadata=_demo_metadata()) as trace:
287
+ state = {"question": "How should agent runtimes be profiled?"}
288
+ with trace_model_call(
289
+ "planner", model="gpt-4.1-mini", input_text=state["question"], output_tokens=120
290
+ ):
291
+ sleep_ms(160)
292
+ state = retrieve_sources(state)
293
+ state = rank_sources(state)
294
+ with trace_model_call(
295
+ "final_answer", model="gpt-4.1-mini", input_text=str(state), output_tokens=300
296
+ ):
297
+ sleep_ms(240)
298
+ trace.export_json(out_path)
299
+ trace.print_report()
300
+ return out_path
agentloop/doctor.py ADDED
@@ -0,0 +1,341 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import os
5
+ import platform
6
+ import socket
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from urllib.error import URLError
12
+ from urllib.parse import urlsplit
13
+ from urllib.request import Request, urlopen
14
+
15
+ from agentloop.config import postgres_connection_source
16
+ from agentloop.runtime import get_runtime_config
17
+ from agentloop.store import get_store
18
+
19
+
20
+ @dataclass
21
+ class DoctorCheck:
22
+ name: str
23
+ status: str
24
+ detail: str
25
+ fix: str | None = None
26
+
27
+ def to_dict(self) -> dict[str, Any]:
28
+ payload = {"name": self.name, "status": self.status, "detail": self.detail}
29
+ if self.fix:
30
+ payload["fix"] = self.fix
31
+ return payload
32
+
33
+
34
+ def _ok(name: str, detail: str) -> DoctorCheck:
35
+ return DoctorCheck(name=name, status="ok", detail=detail)
36
+
37
+
38
+ def _warn(name: str, detail: str, fix: str | None = None) -> DoctorCheck:
39
+ return DoctorCheck(name=name, status="warn", detail=detail, fix=fix)
40
+
41
+
42
+ def _fail(name: str, detail: str, fix: str | None = None) -> DoctorCheck:
43
+ return DoctorCheck(name=name, status="fail", detail=detail, fix=fix)
44
+
45
+
46
+ def _can_connect(host: str, port: int, timeout: float = 1.0) -> bool:
47
+ try:
48
+ with socket.create_connection((host, port), timeout=timeout):
49
+ return True
50
+ except OSError:
51
+ return False
52
+
53
+
54
+ def _health_check(api_url: str, timeout: float = 2.0) -> DoctorCheck:
55
+ parsed = urlsplit(api_url)
56
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
57
+ return _warn(
58
+ "api_health",
59
+ "AGENTLOOP_API_URL must use http:// or https://",
60
+ "Set AGENTLOOP_API_URL to the AgentLoop HTTP API origin.",
61
+ )
62
+ url = api_url.rstrip("/") + "/health"
63
+ try:
64
+ request = Request(url, headers={"User-Agent": "agentloop-doctor"})
65
+ with urlopen( # nosec B310 - API URL scheme is validated above.
66
+ request, timeout=timeout
67
+ ) as response:
68
+ if response.status == 200:
69
+ return _ok("api_health", f"{url} returned HTTP 200")
70
+ return _warn("api_health", f"{url} returned HTTP {response.status}")
71
+ except URLError as exc:
72
+ return _warn(
73
+ "api_health",
74
+ f"Could not reach {url}: {exc.reason}",
75
+ "Start the API with: agentloop server --host 127.0.0.1 --port 8000",
76
+ )
77
+ except Exception as exc: # pragma: no cover - defensive for platform SSL/socket edge cases
78
+ return _warn("api_health", f"Could not reach {url}: {exc}")
79
+
80
+
81
+ def _ready_check(api_url: str, timeout: float = 2.0) -> DoctorCheck:
82
+ parsed = urlsplit(api_url)
83
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
84
+ return _fail(
85
+ "api_ready",
86
+ "AGENTLOOP_API_URL must use http:// or https://",
87
+ "Set AGENTLOOP_API_URL to the deployed AgentLoop HTTP API origin.",
88
+ )
89
+ url = api_url.rstrip("/") + "/readyz"
90
+ try:
91
+ request = Request(url, headers={"User-Agent": "agentloop-production-check"})
92
+ with urlopen( # nosec B310 - API URL scheme is validated above.
93
+ request, timeout=timeout
94
+ ) as response:
95
+ if response.status == 200:
96
+ return _ok("api_ready", f"{url} returned HTTP 200")
97
+ return _fail("api_ready", f"{url} returned HTTP {response.status}")
98
+ except URLError as exc:
99
+ return _fail(
100
+ "api_ready",
101
+ f"Could not reach {url}: {exc.reason}",
102
+ "Deploy the API and confirm /readyz is reachable.",
103
+ )
104
+ except Exception as exc: # pragma: no cover - defensive for platform SSL/socket edge cases
105
+ return _fail("api_ready", f"Could not reach {url}: {exc}")
106
+
107
+
108
+ def _env_enabled(name: str) -> bool:
109
+ return os.getenv(name, "").lower() in {"1", "true", "yes", "on"}
110
+
111
+
112
+ def run_doctor(*, check_api: bool = True, runs_dir: Path | str = "runs") -> dict[str, Any]:
113
+ """Run local setup checks, probing hosted wiring only when it is configured."""
114
+
115
+ checks: list[DoctorCheck] = []
116
+
117
+ py_version = sys.version_info
118
+ if py_version >= (3, 10):
119
+ checks.append(_ok("python", f"Python {platform.python_version()}"))
120
+ else:
121
+ checks.append(
122
+ _fail(
123
+ "python",
124
+ f"Python {platform.python_version()} is too old",
125
+ "Use Python 3.10 or newer.",
126
+ )
127
+ )
128
+
129
+ # These are the actual core runtime dependencies in pyproject.toml. Server,
130
+ # dashboard, Postgres, and instrumentation dependencies are checked by their
131
+ # corresponding modes instead of making a minimal local install look broken.
132
+ for module_name in ["typer", "rich"]:
133
+ try:
134
+ importlib.import_module(module_name)
135
+ checks.append(_ok(f"dependency:{module_name}", "importable"))
136
+ except ImportError:
137
+ checks.append(
138
+ _fail(
139
+ f"dependency:{module_name}",
140
+ "not installed",
141
+ "Install the package with: python -m pip install agentloop-profiler",
142
+ )
143
+ )
144
+
145
+ out_dir = Path(runs_dir)
146
+ try:
147
+ out_dir.mkdir(parents=True, exist_ok=True)
148
+ probe = out_dir / ".agentloop_write_probe"
149
+ probe.write_text("ok", encoding="utf-8")
150
+ probe.unlink(missing_ok=True)
151
+ checks.append(_ok("runs_dir", f"{out_dir} is writable"))
152
+ except Exception as exc:
153
+ checks.append(
154
+ _fail(
155
+ "runs_dir",
156
+ f"{out_dir} is not writable: {exc}",
157
+ "Choose a writable path or set AGENTLOOP_EXPORT_DIR.",
158
+ )
159
+ )
160
+
161
+ try:
162
+ db = get_store()
163
+ db.init()
164
+ checks.append(_ok("store", f"{db.__class__.__name__} initialized"))
165
+ except Exception as exc:
166
+ checks.append(
167
+ _fail(
168
+ "store",
169
+ f"store initialization failed: {exc}",
170
+ "Check AGENTLOOP_STORE_BACKEND and database credentials.",
171
+ )
172
+ )
173
+
174
+ cfg = get_runtime_config()
175
+ auth_required = _env_enabled("AGENTLOOP_REQUIRE_API_KEY")
176
+ explicit_api_url = os.getenv("AGENTLOOP_API_URL")
177
+
178
+ if cfg.auto_upload and auth_required and not cfg.api_key:
179
+ checks.append(
180
+ _warn(
181
+ "api_key",
182
+ "auto upload is enabled but AGENTLOOP_API_KEY is empty",
183
+ "Create a key with: agentloop create-api-key --project-id demo",
184
+ )
185
+ )
186
+ elif cfg.api_key:
187
+ checks.append(_ok("api_key", "AGENTLOOP_API_KEY is configured"))
188
+ else:
189
+ checks.append(_ok("api_key", "not configured; not required for local-only use"))
190
+
191
+ if cfg.auto_upload:
192
+ checks.append(_ok("auto_upload", f"enabled -> {cfg.api_url}"))
193
+ else:
194
+ checks.append(_ok("auto_upload", "disabled; traces stay local"))
195
+
196
+ if check_api:
197
+ if explicit_api_url or cfg.auto_upload:
198
+ checks.append(_health_check(cfg.api_url))
199
+ else:
200
+ checks.append(_ok("api_health", "skipped; no hosted API is configured"))
201
+
202
+ failed = sum(1 for check in checks if check.status == "fail")
203
+ warned = sum(1 for check in checks if check.status == "warn")
204
+ return {
205
+ "ok": failed == 0,
206
+ "failed": failed,
207
+ "warnings": warned,
208
+ "checks": [check.to_dict() for check in checks],
209
+ }
210
+
211
+
212
+ def run_production_check(
213
+ *,
214
+ check_api: bool = True,
215
+ check_store: bool = True,
216
+ allow_http: bool = False,
217
+ ) -> dict[str, Any]:
218
+ """Run strict checks for an internet-facing AgentLoop API/dashboard deployment."""
219
+
220
+ checks: list[DoctorCheck] = []
221
+
222
+ py_version = sys.version_info
223
+ if py_version >= (3, 10):
224
+ checks.append(_ok("python", f"Python {platform.python_version()}"))
225
+ else:
226
+ checks.append(
227
+ _fail(
228
+ "python",
229
+ f"Python {platform.python_version()} is too old",
230
+ "Use Python 3.10 or newer.",
231
+ )
232
+ )
233
+
234
+ backend = os.getenv("AGENTLOOP_STORE_BACKEND", "sqlite").lower()
235
+ if backend == "postgres":
236
+ checks.append(_ok("store_backend", "postgres"))
237
+ else:
238
+ checks.append(
239
+ _fail(
240
+ "store_backend",
241
+ f"{backend} is not production-ready",
242
+ "Set AGENTLOOP_STORE_BACKEND=postgres.",
243
+ )
244
+ )
245
+
246
+ database_source = postgres_connection_source()
247
+ if database_source:
248
+ checks.append(_ok("database_url", f"configured via {database_source}"))
249
+ else:
250
+ checks.append(
251
+ _fail(
252
+ "database_url",
253
+ "missing",
254
+ "Set AGENTLOOP_DATABASE_URL/DATABASE_URL or libpq PGHOST/PGSERVICE variables.",
255
+ )
256
+ )
257
+
258
+ if check_store:
259
+ try:
260
+ db = get_store()
261
+ db.init()
262
+ checks.append(_ok("store", f"{db.__class__.__name__} initialized"))
263
+ except Exception as exc:
264
+ checks.append(
265
+ _fail(
266
+ "store",
267
+ f"store initialization failed: {exc}",
268
+ "Check database credentials and network access.",
269
+ )
270
+ )
271
+
272
+ if _env_enabled("AGENTLOOP_REQUIRE_API_KEY"):
273
+ checks.append(_ok("api_auth", "AGENTLOOP_REQUIRE_API_KEY=true"))
274
+ else:
275
+ checks.append(
276
+ _fail("api_auth", "API key auth is disabled", "Set AGENTLOOP_REQUIRE_API_KEY=true.")
277
+ )
278
+
279
+ admin_key = os.getenv("AGENTLOOP_ADMIN_API_KEY", "")
280
+ weak_admin_values = {
281
+ "",
282
+ "change-me-admin-key",
283
+ "replace-with-a-long-random-admin-secret",
284
+ "dev-secret",
285
+ "admin-secret",
286
+ }
287
+ if admin_key in weak_admin_values or len(admin_key) < 32:
288
+ checks.append(
289
+ _fail(
290
+ "admin_api_key",
291
+ "missing or too weak",
292
+ "Set AGENTLOOP_ADMIN_API_KEY to a generated secret with at least 32 characters.",
293
+ )
294
+ )
295
+ else:
296
+ checks.append(_ok("admin_api_key", "configured"))
297
+
298
+ cors_origins = [
299
+ origin.strip()
300
+ for origin in os.getenv("AGENTLOOP_CORS_ORIGINS", "").split(",")
301
+ if origin.strip()
302
+ ]
303
+ if not cors_origins:
304
+ checks.append(
305
+ _fail("cors_origins", "missing", "Set AGENTLOOP_CORS_ORIGINS to the dashboard origin.")
306
+ )
307
+ elif "*" in cors_origins:
308
+ checks.append(
309
+ _fail(
310
+ "cors_origins",
311
+ "wildcard origin is not allowed for production",
312
+ "Use exact HTTPS origins.",
313
+ )
314
+ )
315
+ else:
316
+ checks.append(_ok("cors_origins", ", ".join(cors_origins)))
317
+
318
+ api_url = os.getenv("AGENTLOOP_API_URL", "")
319
+ if not api_url:
320
+ checks.append(_fail("api_url", "missing", "Set AGENTLOOP_API_URL to the public API URL."))
321
+ elif api_url.startswith("https://"):
322
+ checks.append(_ok("api_url", api_url))
323
+ elif allow_http:
324
+ checks.append(
325
+ _warn("api_url", f"{api_url} is not HTTPS", "Use HTTPS for production traffic.")
326
+ )
327
+ else:
328
+ checks.append(_fail("api_url", f"{api_url} is not HTTPS", "Use an HTTPS public API URL."))
329
+
330
+ if check_api and api_url:
331
+ checks.append(_health_check(api_url))
332
+ checks.append(_ready_check(api_url))
333
+
334
+ failed = sum(1 for check in checks if check.status == "fail")
335
+ warned = sum(1 for check in checks if check.status == "warn")
336
+ return {
337
+ "ok": failed == 0,
338
+ "failed": failed,
339
+ "warnings": warned,
340
+ "checks": [check.to_dict() for check in checks],
341
+ }