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
dashboard/app.py ADDED
@@ -0,0 +1,922 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ import streamlit as st
8
+
9
+ from agentloop.ci import build_ci_report, ci_report_to_markdown
10
+ from agentloop.costs import format_cost_usd
11
+ from agentloop.demo import run_baseline, run_langgraph_style, run_optimized, run_proof_pair
12
+ from agentloop.findings import build_diagnosis
13
+ from agentloop.issues import build_issue_drafts, issue_drafts_to_markdown
14
+ from agentloop.operations import operation_kind
15
+ from agentloop.optimizer import build_optimization_plan
16
+ from agentloop.patches import RepositoryPathError, build_patch_plan
17
+ from agentloop.quality import (
18
+ QualityValidationError,
19
+ build_quality_report,
20
+ parse_quality_fixtures,
21
+ quality_report_to_markdown,
22
+ )
23
+ from agentloop.ranking import SORT_FIELDS, sort_ranked
24
+ from agentloop.replay import ReplayGates, build_replay_report
25
+ from agentloop.store import (
26
+ ALLOWED_FINDING_TRANSITIONS,
27
+ FindingNotFoundError,
28
+ FindingTransitionError,
29
+ get_store,
30
+ )
31
+ from agentloop.tracer import AgentTrace
32
+ from agentloop.value import build_value_report
33
+
34
+ try:
35
+ from dashboard.trace_upload import TraceUploadError, parse_uploaded_trace
36
+ from dashboard.value_view import assumption_inputs, percentage, render_value_report
37
+ except ModuleNotFoundError:
38
+ from trace_upload import TraceUploadError, parse_uploaded_trace
39
+ from value_view import assumption_inputs, percentage, render_value_report
40
+
41
+ st.set_page_config(page_title="AgentLoop Dashboard", layout="wide")
42
+
43
+ runs_dir = Path("runs")
44
+ runs_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+
47
+ @st.cache_resource
48
+ def load_store():
49
+ db = get_store()
50
+ db.init()
51
+ return db
52
+
53
+
54
+ def money(value: float | None) -> str:
55
+ return "unavailable" if value is None else f"${float(value):,.4f}"
56
+
57
+
58
+ def seconds(ms: float | None) -> str:
59
+ return "unavailable" if ms is None else f"{float(ms) / 1000:,.2f}s"
60
+
61
+
62
+ def render_estimate_details(item: dict) -> None:
63
+ if item.get("evidence_level"):
64
+ st.write(f"Evidence level: {item['evidence_level']}")
65
+ estimate = item.get("estimate")
66
+ if estimate:
67
+ calibration = "calibrated" if estimate.get("calibrated") is True else "uncalibrated"
68
+ st.write(
69
+ f"Estimator: {estimate['estimator_id']} version {estimate['estimator_version']} "
70
+ f"({estimate['method']}, {calibration}; predicted savings)"
71
+ )
72
+ st.write("Assumptions: " + "; ".join(estimate.get("assumptions", [])))
73
+ st.caption(estimate["formula"])
74
+ st.json(
75
+ {"parameters": estimate.get("parameters", {}), "inputs": estimate.get("inputs", {})}
76
+ )
77
+ elif item.get("evidence_level"):
78
+ st.write("Assumptions: " + "; ".join(item.get("assumptions", [])))
79
+ st.caption(item.get("estimate_formula") or item.get("savings", {}).get("formula", ""))
80
+
81
+
82
+ def load_trace_for_project(run_id: str, project_id: str) -> AgentTrace | None:
83
+ return load_store().get_trace(run_id=run_id, project_id=project_id)
84
+
85
+
86
+ def select_trace(traces: list[dict], label: str = "Choose trace") -> AgentTrace | None:
87
+ labels = {f"{t['name']} · {t['run_id']}": t["run_id"] for t in traces}
88
+ selected_label = st.selectbox(label, list(labels.keys()))
89
+ return load_trace_for_project(labels[selected_label], project_id)
90
+
91
+
92
+ def trace_options(traces: list[dict]) -> dict[str, str]:
93
+ return {f"{t['name']} - {t['run_id']}": t["run_id"] for t in traces}
94
+
95
+
96
+ def selected_trace_from_options(traces: list[dict], label: str) -> AgentTrace | None:
97
+ options = trace_options(traces)
98
+ selected = st.selectbox(label, list(options.keys()))
99
+ return load_trace_for_project(options[selected], project_id)
100
+
101
+
102
+ def render_gate_table(results: list[dict]) -> None:
103
+ rows = [
104
+ {
105
+ "gate": item["name"],
106
+ "status": "pass" if item["passed"] else "fail",
107
+ "detail": item["detail"],
108
+ }
109
+ for item in results
110
+ ]
111
+ st.dataframe(pd.DataFrame(rows), width="stretch", hide_index=True)
112
+
113
+
114
+ st.sidebar.title("AgentLoop")
115
+ st.sidebar.caption("Execution profiling and evidence")
116
+
117
+ project_id = st.sidebar.text_input("Project", value=st.session_state.get("project_id", "default"))
118
+ st.session_state["project_id"] = project_id
119
+
120
+ page = st.sidebar.radio(
121
+ "Navigate",
122
+ [
123
+ "Overview",
124
+ "Traces",
125
+ "Optimization Queue",
126
+ "Optimization",
127
+ "Diagnosis",
128
+ "Patch Plan",
129
+ "Replay Proof",
130
+ "Quality Gates",
131
+ "Value & Pricing",
132
+ "API Keys",
133
+ "Ingest",
134
+ "Setup",
135
+ ],
136
+ )
137
+
138
+ store = load_store()
139
+
140
+ st.title("AgentLoop Dashboard")
141
+ st.caption("Cost, latency, and execution-graph optimization for production AI agents")
142
+
143
+ if page == "Overview":
144
+ summary = store.usage_summary(project_id=project_id)
145
+ traces = store.list_traces(project_id=project_id)
146
+
147
+ c1, c2, c3, c4 = st.columns(4)
148
+ c1.metric("Runs", int(summary.get("run_count", 0)))
149
+ c2.metric("Total runtime", seconds(summary.get("total_runtime_ms", 0)))
150
+ c3.metric(
151
+ "Estimated LLM cost",
152
+ format_cost_usd(summary.get("estimated_cost_usd"), summary.get("cost_status", "complete")),
153
+ )
154
+ c4.metric("Retries", int(summary.get("retry_count", 0)))
155
+
156
+ c5, c6, c7, c8 = st.columns(4)
157
+ c5.metric("Input tokens", int(summary.get("input_tokens", 0)))
158
+ c6.metric("Output tokens", int(summary.get("output_tokens", 0)))
159
+ c7.metric("Model calls", int(summary.get("model_call_count", 0)))
160
+ c8.metric("Tool calls", int(summary.get("tool_call_count", 0)))
161
+
162
+ st.subheader("Recent runs")
163
+ if traces:
164
+ df = pd.DataFrame(traces)
165
+ st.dataframe(df, width="stretch")
166
+
167
+ chart_df = df[["name", "total_runtime_ms"]].copy()
168
+ chart_df["runtime_s"] = chart_df["total_runtime_ms"] / 1000
169
+ st.subheader("Runtime by run")
170
+ st.bar_chart(chart_df, x="name", y="runtime_s")
171
+ else:
172
+ st.info(
173
+ "No stored traces yet. Use the Ingest page or run "
174
+ "`uv run agentloop store-trace --path TRACE.json`."
175
+ )
176
+
177
+ elif page == "Traces":
178
+ st.subheader("Stored traces")
179
+
180
+ cursor_key = f"traces_cursor_stack::{project_id}"
181
+ if cursor_key not in st.session_state:
182
+ st.session_state[cursor_key] = [None] # cursor to fetch the *current* page
183
+
184
+ page_index = len(st.session_state[cursor_key]) - 1
185
+ page_size = st.number_input("Page size", min_value=5, max_value=200, value=25, step=5)
186
+ page = store.list_traces_page(
187
+ project_id=project_id, limit=int(page_size), cursor=st.session_state[cursor_key][-1]
188
+ )
189
+ traces = page["items"]
190
+
191
+ nav_cols = st.columns([1, 1, 6])
192
+ if nav_cols[0].button("◀ Previous", disabled=page_index == 0):
193
+ st.session_state[cursor_key].pop()
194
+ st.rerun()
195
+ if nav_cols[1].button("Next ▶", disabled=page["next_cursor"] is None):
196
+ st.session_state[cursor_key].append(page["next_cursor"])
197
+ st.rerun()
198
+ st.caption(f"Page {page_index + 1}")
199
+
200
+ if not traces:
201
+ st.info("No traces stored for this project yet.")
202
+ else:
203
+ df = pd.DataFrame(traces)
204
+ st.dataframe(df, width="stretch")
205
+ selected_run = st.selectbox("Open run", df["run_id"].tolist())
206
+ trace = load_trace_for_project(selected_run, project_id)
207
+ if trace is not None:
208
+ report = trace.report()
209
+ st.markdown(f"### {trace.name}")
210
+ if report.get("execution") is not None:
211
+ execution = report["execution"]
212
+ st.caption(
213
+ f"Workflow {execution.get('workflow_id', 'unknown')} · version {execution.get('version', 'unknown')} · {execution.get('status', 'unknown')}"
214
+ )
215
+ with st.expander("Workflow references"):
216
+ st.json(execution)
217
+ c1, c2, c3, c4 = st.columns(4)
218
+ c1.metric("Runtime", seconds(report["total_runtime_ms"]))
219
+ c2.metric(
220
+ "Recorded model cost" if "execution" in report else "Cost",
221
+ format_cost_usd(report["estimated_cost_usd"], report.get("cost_status")),
222
+ )
223
+ c3.metric("Model calls", report["model_call_count"])
224
+ c4.metric("Tool calls", report["tool_call_count"])
225
+
226
+ events = pd.DataFrame(report["events"])
227
+ st.subheader("Event timeline")
228
+ if not events.empty:
229
+ cols = [
230
+ "event_type",
231
+ "name",
232
+ "duration_ms",
233
+ "model",
234
+ "input_tokens",
235
+ "output_tokens",
236
+ "status",
237
+ ]
238
+ if "execution" in report or "stages" in report:
239
+ events["operation_kind"] = [operation_kind(event) for event in trace.events]
240
+ summaries = [
241
+ report.get("stages", {}).get(event.event_id, {}) for event in trace.events
242
+ ]
243
+ stage_columns = {
244
+ "Stage": "stage_id",
245
+ "Stage kind": "kind",
246
+ "Stage version": "version",
247
+ "Input schema": "input_schema_ref",
248
+ "Output schema": "output_schema_ref",
249
+ "Input reference": "input_ref",
250
+ "Output reference": "output_ref",
251
+ "Outcome": "outcome",
252
+ "Dependencies": "depends_on",
253
+ }
254
+ for label, key in stage_columns.items():
255
+ events[label] = [value.get(key) for value in summaries]
256
+ cols = [
257
+ "operation_kind",
258
+ "Stage",
259
+ "Stage kind",
260
+ "Stage version",
261
+ "parent_id",
262
+ *cols,
263
+ "Input schema",
264
+ "Output schema",
265
+ "Input reference",
266
+ "Output reference",
267
+ "Outcome",
268
+ "Dependencies",
269
+ ]
270
+ for col in cols:
271
+ if col not in events.columns:
272
+ events[col] = None
273
+ st.dataframe(events[cols], width="stretch")
274
+
275
+ st.download_button(
276
+ "Download trace JSON",
277
+ data=json.dumps(trace.to_dict(), indent=2),
278
+ file_name=f"{trace.run_id}.json",
279
+ mime="application/json",
280
+ )
281
+
282
+ elif page == "Optimization Queue":
283
+ st.subheader("Optimization queue")
284
+ dimension = st.selectbox(
285
+ "Sort investigations by", SORT_FIELDS, format_func=lambda value: value.replace("_", " ")
286
+ )
287
+ queue = sort_ranked(store.optimization_queue(project_id=project_id), dimension)
288
+ findings = store.list_findings(project_id=project_id)
289
+
290
+ c1, c2, c3, c4 = st.columns(4)
291
+ c1.metric("Open opportunities", len(queue))
292
+ c2.metric("Persisted findings", len(findings))
293
+ c3.metric("Patchable findings", sum(1 for finding in findings if finding["patchable"]))
294
+ c4.metric(
295
+ "Estimated latency savings",
296
+ seconds(
297
+ None
298
+ if any(item["estimated_latency_savings_ms"] is None for item in queue)
299
+ else sum(item["estimated_latency_savings_ms"] for item in queue)
300
+ ),
301
+ )
302
+
303
+ if not queue:
304
+ st.info(
305
+ "No optimization opportunities have been persisted yet. Store traces or run diagnosis first."
306
+ )
307
+ else:
308
+ queue_df = pd.DataFrame(queue)
309
+ queue_df["readiness"] = [item["ranking"]["status"].replace("_", " ") for item in queue]
310
+ visible_cols = [
311
+ "priority_score",
312
+ "readiness",
313
+ "severity",
314
+ "type",
315
+ "title",
316
+ "occurrence_count",
317
+ "run_count",
318
+ "patchable_count",
319
+ "quality_risk",
320
+ "requires_scorer",
321
+ "safe_to_auto_patch",
322
+ "estimated_latency_savings_ms",
323
+ "estimated_cost_savings_usd",
324
+ ]
325
+ st.dataframe(queue_df[visible_cols], width="stretch", hide_index=True)
326
+
327
+ selected_title = st.selectbox("Open queue item", queue_df["title"].tolist())
328
+ item = next(row for row in queue if row["title"] == selected_title)
329
+ st.markdown(f"### {item['title']}")
330
+ c5, c6, c7, c8 = st.columns(4)
331
+ c5.metric("Investigation rank", item["ranking"]["priority_rank"])
332
+ c6.metric("Severity", item["severity"])
333
+ c7.metric("Affected runs", item["run_count"])
334
+ c8.metric("Patchable", item["patchable_count"])
335
+ st.write(f"Type: `{item['type']}`")
336
+ st.write(f"Status: `{item['status']}`")
337
+ st.write(f"Quality risk: `{item['quality_risk']}`")
338
+ st.write("Test readiness: " + item["ranking"]["status"].replace("_", " "))
339
+ with st.expander("Ranking inputs and provenance"):
340
+ st.json(item["ranking"])
341
+ st.write(f"Requires scorer: `{item['requires_scorer']}`")
342
+ st.write(f"Safe to auto-patch: `{item['safe_to_auto_patch']}`")
343
+ st.write(
344
+ f"Estimated savings: {seconds(item['estimated_latency_savings_ms'])} / {money(item['estimated_cost_savings_usd'])}"
345
+ )
346
+ st.write("Affected runs")
347
+ st.code("\n".join(item["affected_runs"]), language="text")
348
+
349
+ item_findings = [
350
+ finding
351
+ for finding in findings
352
+ if finding["type"] == item["type"] and finding["title"] == item["title"]
353
+ ]
354
+ if item_findings:
355
+ st.subheader("Finding instances")
356
+ st.dataframe(pd.DataFrame(item_findings), width="stretch", hide_index=True)
357
+
358
+ st.markdown("**Update finding status**")
359
+ finding_labels = {
360
+ f"{f['run_id']} · {f['finding_id']} (currently {f['status']})": f
361
+ for f in item_findings
362
+ }
363
+ selected_finding_label = st.selectbox(
364
+ "Finding", list(finding_labels.keys()), key=f"finding_select_{item['queue_id']}"
365
+ )
366
+ selected_finding = finding_labels[selected_finding_label]
367
+ allowed_targets = sorted(
368
+ ALLOWED_FINDING_TRANSITIONS.get(selected_finding["status"], ())
369
+ )
370
+ if allowed_targets:
371
+ new_status = st.selectbox(
372
+ "New status", allowed_targets, key=f"status_select_{item['queue_id']}"
373
+ )
374
+ if st.button("Apply transition", key=f"apply_transition_{item['queue_id']}"):
375
+ try:
376
+ store.update_finding_status(
377
+ project_id,
378
+ selected_finding["run_id"],
379
+ selected_finding["finding_id"],
380
+ new_status,
381
+ )
382
+ except (FindingNotFoundError, FindingTransitionError) as exc:
383
+ st.error(str(exc))
384
+ else:
385
+ st.success(f"{selected_finding['finding_id']} is now {new_status}")
386
+ st.rerun()
387
+ else:
388
+ st.caption(f"No transitions available from '{selected_finding['status']}'.")
389
+
390
+ st.download_button(
391
+ "Download optimization queue JSON",
392
+ data=json.dumps({"project_id": project_id, "queue": queue}, indent=2),
393
+ file_name="agentloop_optimization_queue.json",
394
+ mime="application/json",
395
+ )
396
+
397
+ st.subheader("GitHub issue drafts")
398
+ drafts = build_issue_drafts(queue)
399
+ if not drafts:
400
+ st.info("No patchable queue items available for issue drafts.")
401
+ else:
402
+ selected_issue = st.selectbox(
403
+ "Preview issue draft", [draft["title"] for draft in drafts]
404
+ )
405
+ draft = next(item for item in drafts if item["title"] == selected_issue)
406
+ st.write(f"Labels: `{', '.join(draft['labels'])}`")
407
+ st.code(draft["body"], language="markdown")
408
+ st.download_button(
409
+ "Download issue drafts Markdown",
410
+ data=issue_drafts_to_markdown(drafts),
411
+ file_name="agentloop_issue_drafts.md",
412
+ mime="text/markdown",
413
+ )
414
+
415
+ elif page == "Optimization":
416
+ traces = store.list_traces(project_id=project_id)
417
+ st.subheader("Optimization plans")
418
+ if not traces:
419
+ st.info("No traces stored for this project yet.")
420
+ else:
421
+ trace = select_trace(traces)
422
+ if trace is not None:
423
+ report = trace.report()
424
+ plan = build_optimization_plan(trace, report)
425
+ if (
426
+ plan.get("savings_aggregation", {}).get("latency_estimate_complete") is False
427
+ or plan.get("savings_aggregation", {}).get("cost_estimate_complete") is False
428
+ ):
429
+ st.caption("Some savings are unavailable; totals cover modeled candidates only.")
430
+ current = plan["current"]
431
+ after = plan["estimated_after"]
432
+
433
+ c1, c2, c3, c4 = st.columns(4)
434
+ c1.metric("Current runtime", seconds(current["runtime_ms"]))
435
+ c2.metric("Estimated runtime", seconds(after["runtime_ms"]))
436
+ c3.metric("Latency reduction", f"{after['latency_reduction_pct']:.1f}%")
437
+ c4.metric("Cost reduction", percentage(after["cost_reduction_pct"]))
438
+
439
+ with st.expander("Show value and pricing estimate", expanded=False):
440
+ rpm, rate, incident_cost = assumption_inputs("optimization_")
441
+ value = build_value_report(
442
+ trace,
443
+ runs_per_month=rpm,
444
+ engineer_hourly_rate_usd=rate,
445
+ incident_cost_usd=incident_cost,
446
+ )
447
+ render_value_report(value, show_download=False)
448
+
449
+ st.subheader("Optimization cards")
450
+ if plan.get("rule_errors"):
451
+ st.warning(
452
+ "Analysis incomplete; failed rules: "
453
+ + ", ".join(error["rule_id"] for error in plan["rule_errors"])
454
+ )
455
+ cards = plan["optimization_cards"]
456
+ if not cards:
457
+ st.info("No major optimization cards detected yet.")
458
+ for card in cards:
459
+ with st.expander(
460
+ f"{card['title']} · confidence: {card['confidence']}", expanded=True
461
+ ):
462
+ st.write(card["why"])
463
+ render_estimate_details(card)
464
+ st.code(card["rewrite_hint"])
465
+ st.write(
466
+ f"Estimated latency savings: {seconds(card['estimated_latency_savings_ms'])}"
467
+ )
468
+ st.write(f"Estimated cost savings: {money(card['estimated_cost_savings_usd'])}")
469
+
470
+ st.subheader("Execution graph")
471
+ graph = plan["graph"]
472
+ if graph.get("execution") is not None:
473
+ st.json(graph["execution"])
474
+ if graph.get("dependency_evidence") is not None:
475
+ st.json(graph["dependency_evidence"])
476
+ st.write("Bottlenecks")
477
+ st.dataframe(pd.DataFrame(graph["bottlenecks"]), width="stretch")
478
+ st.write("Parallelization candidates")
479
+ if graph["parallelizable_groups"]:
480
+ st.dataframe(pd.DataFrame(graph["parallelizable_groups"]), width="stretch")
481
+ else:
482
+ st.info(
483
+ "No repeated tool-call groups currently qualify as parallelization candidates."
484
+ )
485
+ st.write("Edges")
486
+ st.dataframe(pd.DataFrame(graph["edges"]), width="stretch")
487
+
488
+ st.download_button(
489
+ "Download optimization plan JSON",
490
+ data=json.dumps(plan, indent=2),
491
+ file_name="agentloop_optimization_plan.json",
492
+ mime="application/json",
493
+ )
494
+
495
+ elif page == "Diagnosis":
496
+ traces = store.list_traces(project_id=project_id)
497
+ st.subheader("Machine-actionable diagnosis")
498
+ if not traces:
499
+ st.info("No traces stored for this project yet.")
500
+ else:
501
+ trace = selected_trace_from_options(traces, "Choose trace for diagnosis")
502
+ if trace is not None:
503
+ dimension = st.selectbox(
504
+ "Sort findings by", SORT_FIELDS, format_func=lambda value: value.replace("_", " ")
505
+ )
506
+ diagnosis = build_diagnosis(trace, sort_by=dimension)
507
+ if diagnosis.get("rule_errors"):
508
+ st.warning(
509
+ "Analysis incomplete; failed rules: "
510
+ + ", ".join(error["rule_id"] for error in diagnosis["rule_errors"])
511
+ )
512
+ summary = diagnosis["summary"]
513
+ c1, c2, c3, c4 = st.columns(4)
514
+ c1.metric("Findings", summary["finding_count"])
515
+ c2.metric("High severity", summary["high_severity_count"])
516
+ c3.metric("Patchable", summary["patchable_count"])
517
+ c4.metric("Latency reduction", f"{summary['estimated_latency_reduction_pct']:.1f}%")
518
+
519
+ findings = diagnosis.get("findings", [])
520
+ if not findings:
521
+ st.info("No machine-actionable findings detected for this trace.")
522
+ for finding in findings:
523
+ label = f"{finding['severity'].upper()} - {finding['title']} ({finding['type']})"
524
+ with st.expander(label, expanded=finding["severity"] == "high"):
525
+ st.write(finding["metadata"].get("why", ""))
526
+ render_estimate_details(finding)
527
+ st.write("Test readiness: " + finding["ranking"]["status"].replace("_", " "))
528
+ st.json(finding["ranking"])
529
+ st.write(f"Finding ID: `{finding['finding_id']}`")
530
+ st.write(f"Confidence: `{finding['confidence']}`")
531
+ st.write(f"Affected spans: `{', '.join(finding['affected_spans'])}`")
532
+ st.write(f"Rewrite: {finding['rewrite']['hint']}")
533
+ st.write(f"Validation: {finding['validation']['acceptance_criteria']}")
534
+ st.write(
535
+ "Estimated savings: "
536
+ f"{seconds(finding['savings']['estimated_latency_savings_ms'])}, "
537
+ f"{money(finding['savings']['estimated_cost_savings_usd'])}"
538
+ )
539
+ if finding["evidence"]:
540
+ st.dataframe(
541
+ pd.DataFrame(finding["evidence"]), width="stretch", hide_index=True
542
+ )
543
+
544
+ st.download_button(
545
+ "Download diagnosis JSON",
546
+ data=json.dumps(diagnosis, indent=2),
547
+ file_name="agentloop_diagnosis.json",
548
+ mime="application/json",
549
+ )
550
+
551
+ elif page == "Patch Plan":
552
+ traces = store.list_traces(project_id=project_id)
553
+ st.subheader("Trace-to-patch planning")
554
+ if not traces:
555
+ st.info("No traces stored for this project yet.")
556
+ else:
557
+ trace = selected_trace_from_options(traces, "Choose trace for patch planning")
558
+ repo_path = st.text_input("Repository path", value=str(Path.cwd()))
559
+ if trace is not None:
560
+ try:
561
+ plan = build_patch_plan(trace, repo_path=repo_path)
562
+ except RepositoryPathError as exc:
563
+ # The path is free text re-read on every rerun, so a half-typed one
564
+ # reaches build_patch_plan before the operator has finished editing.
565
+ # Only the path error is caught: anything else planning raises is a
566
+ # bug in planning, and swallowing it here would hide it.
567
+ st.error(f"Fix the repository path: {exc}.")
568
+ st.stop()
569
+ summary = plan["summary"]
570
+ c1, c2, c3 = st.columns(3)
571
+ c1.metric("Patch plans", summary["patch_count"])
572
+ c2.metric("Unsupported", summary["unsupported_finding_count"])
573
+ c3.metric("Frameworks", ", ".join(summary["frameworks_detected"]) or "none")
574
+
575
+ for item in plan.get("patch_plans", []):
576
+ with st.expander(f"{item['patch_id']} - {item['title']}", expanded=True):
577
+ st.write(f"Type: `{item['type']}`")
578
+ st.write(f"Risk: `{item['risk']}`")
579
+ st.write(f"Framework: `{item['framework']}`")
580
+ st.write(f"Finding: `{item['finding_id']}`")
581
+ st.write(f"Before pattern: {item['before_pattern']}")
582
+ st.write(f"Proposed rewrite: {item['proposed_rewrite']}")
583
+ st.code(item["suggested_diff"], language="text")
584
+ st.write(f"Validation command: `{item['validation_command']}`")
585
+ st.write(f"Acceptance: {item['acceptance_criteria']}")
586
+ if item["files"]:
587
+ st.dataframe(pd.DataFrame(item["files"]), width="stretch", hide_index=True)
588
+ if item["notes"]:
589
+ for note in item["notes"]:
590
+ st.write(f"- {note}")
591
+
592
+ if plan.get("unsupported_findings"):
593
+ st.subheader("Unsupported findings")
594
+ st.dataframe(
595
+ pd.DataFrame(plan["unsupported_findings"]), width="stretch", hide_index=True
596
+ )
597
+
598
+ st.download_button(
599
+ "Download patch plan JSON",
600
+ data=json.dumps(plan, indent=2),
601
+ file_name="agentloop_patch_plan.json",
602
+ mime="application/json",
603
+ )
604
+
605
+ elif page == "Replay Proof":
606
+ traces = store.list_traces(project_id=project_id)
607
+ st.subheader("Before/after replay proof")
608
+ if len(traces) < 2:
609
+ st.info("Store at least two traces to compare a baseline and candidate.")
610
+ else:
611
+ options = trace_options(traces)
612
+ labels = list(options.keys())
613
+ baseline_label = st.selectbox("Baseline trace", labels, index=0)
614
+ candidate_label = st.selectbox("Candidate trace", labels, index=1 if len(labels) > 1 else 0)
615
+ baseline = load_trace_for_project(options[baseline_label], project_id)
616
+ candidate = load_trace_for_project(options[candidate_label], project_id)
617
+
618
+ st.markdown("#### Gates")
619
+ g1, g2, g3, g4 = st.columns(4)
620
+ min_latency = g1.number_input("Min latency improvement %", min_value=0.0, value=20.0)
621
+ min_cost = g2.number_input("Min cost improvement %", min_value=0.0, value=5.0)
622
+ require_schema = g3.checkbox("Require schema valid", value=False)
623
+ min_quality = g4.number_input("Min quality score", min_value=0.0, max_value=1.0, value=0.0)
624
+ quality_threshold = min_quality if min_quality > 0 else None
625
+
626
+ if baseline is not None and candidate is not None:
627
+ gates = ReplayGates(
628
+ min_latency_improvement_pct=min_latency,
629
+ min_cost_improvement_pct=min_cost,
630
+ require_schema_valid=require_schema,
631
+ min_quality_score=quality_threshold,
632
+ )
633
+ replay = build_replay_report(baseline, candidate, gates=gates)
634
+ ci_report = build_ci_report(baseline, candidate, gates=gates)
635
+ status = "passed" if replay["gates"]["passed"] else "failed"
636
+ c1, c2, c3, c4 = st.columns(4)
637
+ c1.metric("Status", status)
638
+ c2.metric("Latency improvement", f"{replay['deltas']['latency_improvement_pct']:.1f}%")
639
+ cost_improvement = replay["deltas"]["cost_improvement_pct"]
640
+ c3.metric("Cost improvement", percentage(cost_improvement))
641
+ c4.metric("Retry delta", replay["deltas"]["retry_count_delta"])
642
+
643
+ st.subheader("Gate results")
644
+ render_gate_table(replay["gates"]["results"])
645
+
646
+ st.subheader("Metric deltas")
647
+ metric_rows = [
648
+ {"metric": key, "value": value}
649
+ for key, value in replay["deltas"].items()
650
+ if key.endswith("_delta") or key.endswith("_pct")
651
+ ]
652
+ st.dataframe(pd.DataFrame(metric_rows), width="stretch", hide_index=True)
653
+
654
+ st.subheader("PR comment preview")
655
+ st.code(
656
+ "Trace in. Rewrite plan out. Replay proof in the PR.\n\n"
657
+ + ci_report_to_markdown(ci_report)
658
+ )
659
+
660
+ col_json, col_md = st.columns(2)
661
+ col_json.download_button(
662
+ "Download replay JSON",
663
+ data=json.dumps(replay, indent=2),
664
+ file_name="agentloop_replay_report.json",
665
+ mime="application/json",
666
+ )
667
+ col_md.download_button(
668
+ "Download PR Markdown",
669
+ data=ci_report_to_markdown(ci_report),
670
+ file_name="agentloop_ci.md",
671
+ mime="text/markdown",
672
+ )
673
+
674
+ elif page == "Quality Gates":
675
+ traces = store.list_traces(project_id=project_id)
676
+ st.subheader("Quality gates")
677
+ st.caption(
678
+ "Compare caller-defined quality criteria alongside recorded cost and latency. "
679
+ "Legacy scorers and versioned structured decision, multilabel, numeric, extraction and matching scorers are supported. Custom Python scorers are trusted local code."
680
+ )
681
+ if len(traces) < 2:
682
+ st.info("Store at least two traces to score baseline and candidate quality.")
683
+ else:
684
+ options = trace_options(traces)
685
+ labels = list(options.keys())
686
+ default_fixture = [
687
+ {
688
+ "id": "required_summary_fields",
689
+ "input": "Summarize one source.",
690
+ "candidate_output": {
691
+ "summary": "AgentLoop proves rewrites.",
692
+ "sources": ["source-a"],
693
+ },
694
+ "baseline_output": {
695
+ "summary": "AgentLoop proves rewrites.",
696
+ "sources": ["source-a"],
697
+ },
698
+ "scorer": {"type": "required_fields", "required": ["summary", "sources"]},
699
+ }
700
+ ]
701
+ with st.form("quality_gate_form"):
702
+ baseline_label = st.selectbox("Baseline trace", labels, index=0, key="quality_baseline")
703
+ candidate_label = st.selectbox(
704
+ "Candidate trace",
705
+ labels,
706
+ index=1 if len(labels) > 1 else 0,
707
+ key="quality_candidate",
708
+ )
709
+ min_score = st.number_input(
710
+ "Minimum candidate quality score", min_value=0.0, max_value=1.0, value=0.9
711
+ )
712
+ uploaded = st.file_uploader(
713
+ "Upload quality fixture JSON", type=["json"], key="quality_fixture_upload"
714
+ )
715
+ fixtures_text = st.text_area(
716
+ "Fixture JSON",
717
+ value=json.dumps({"fixtures": default_fixture}, indent=2),
718
+ height=260,
719
+ )
720
+ submitted = st.form_submit_button("Run quality gate")
721
+
722
+ if submitted:
723
+ try:
724
+ raw_fixtures = (
725
+ uploaded.read().decode("utf-8") if uploaded is not None else fixtures_text
726
+ )
727
+ fixtures = parse_quality_fixtures(json.loads(raw_fixtures))
728
+ baseline = load_trace_for_project(options[baseline_label], project_id)
729
+ candidate = load_trace_for_project(options[candidate_label], project_id)
730
+ if baseline is None or candidate is None:
731
+ raise QualityValidationError(
732
+ "selected baseline or candidate trace is unavailable"
733
+ )
734
+ quality = build_quality_report(
735
+ fixtures,
736
+ baseline_trace=baseline,
737
+ candidate_trace=candidate,
738
+ min_score=min_score,
739
+ )
740
+ except json.JSONDecodeError as exc:
741
+ st.error(
742
+ "Fix the quality fixture JSON: "
743
+ f"{exc.msg} at line {exc.lineno}, column {exc.colno}."
744
+ )
745
+ except (UnicodeError, QualityValidationError) as exc:
746
+ st.error(f"Fix the quality fixture JSON: {exc}.")
747
+ else:
748
+ status = "passed" if quality["passed"] else "failed"
749
+ c1, c2, c3, c4 = st.columns(4)
750
+ c1.metric("Status", status)
751
+ c2.metric(
752
+ "Candidate score",
753
+ "unavailable"
754
+ if quality["candidate_score"] is None
755
+ else str(quality["candidate_score"])
756
+ if quality.get("schema_version") == "2.0"
757
+ else f"{quality['candidate_score']:.4f}",
758
+ )
759
+ c3.metric(
760
+ "Quality delta",
761
+ "unavailable"
762
+ if quality["quality_delta"] is None
763
+ else str(quality["quality_delta"])
764
+ if quality.get("schema_version") == "2.0"
765
+ else f"{quality['quality_delta']:.4f}",
766
+ )
767
+ c4.metric("Failed cases", quality["failed_case_count"])
768
+ if quality.get("schema_version") == "2.0":
769
+ st.caption(
770
+ f"Indeterminate cases: {quality['indeterminate_case_count']}; incomplete evidence cannot pass the quality gate."
771
+ )
772
+ with st.expander("Quality denominators and classification metrics"):
773
+ st.json(
774
+ {
775
+ "summaries": quality["summaries"],
776
+ "classification": quality["classification"],
777
+ }
778
+ )
779
+ st.dataframe(pd.DataFrame(quality["cases"]), width="stretch", hide_index=True)
780
+ st.download_button(
781
+ "Download quality report JSON",
782
+ data=json.dumps(quality, indent=2),
783
+ file_name="agentloop_quality_report.json",
784
+ mime="application/json",
785
+ )
786
+ st.download_button(
787
+ "Download quality report Markdown",
788
+ data=quality_report_to_markdown(quality),
789
+ file_name="agentloop_quality_report.md",
790
+ mime="text/markdown",
791
+ )
792
+
793
+ elif page == "Value & Pricing":
794
+ traces = store.list_traces(project_id=project_id)
795
+ st.subheader("Value report and pricing scenario")
796
+ st.caption(
797
+ "Turn an agent trace into operational value, reliability risk, and a configurable pricing scenario."
798
+ )
799
+ if not traces:
800
+ st.info(
801
+ "No traces stored for this project yet. Generate demo traces from the Ingest page first."
802
+ )
803
+ else:
804
+ trace = select_trace(traces, label="Choose trace for value report")
805
+ if trace is not None:
806
+ st.markdown("#### Assumptions")
807
+ rpm, rate, incident_cost = assumption_inputs("value_page_")
808
+ value = build_value_report(
809
+ trace,
810
+ runs_per_month=rpm,
811
+ engineer_hourly_rate_usd=rate,
812
+ incident_cost_usd=incident_cost,
813
+ )
814
+ render_value_report(value)
815
+ st.subheader("CLI command")
816
+ st.code(
817
+ "uv run agentloop value-report --path runs/research_agent_baseline.json "
818
+ f"--runs-per-month {rpm} "
819
+ f"--engineer-hourly-rate-usd {rate} "
820
+ f"--incident-cost-usd {incident_cost} "
821
+ "--out runs/value_report.json",
822
+ language="bash",
823
+ )
824
+
825
+ elif page == "API Keys":
826
+ st.subheader("Create project API key")
827
+ st.warning("API keys are only shown once. Copy the generated key before leaving this page.")
828
+ key_name = st.text_input("Key name", value="local-dev")
829
+ if st.button("Create API key"):
830
+ created = store.create_api_key(project_id=project_id, name=key_name)
831
+ st.success(f"Created key for project `{project_id}`")
832
+ st.code(created["api_key"])
833
+
834
+ st.subheader("Use this key")
835
+ st.code(
836
+ "uv run agentloop upload --path runs/research_agent_baseline.json "
837
+ "--api-url http://127.0.0.1:8000 --api-key YOUR_KEY",
838
+ language="bash",
839
+ )
840
+
841
+ elif page == "Ingest":
842
+ st.subheader("Generate or upload traces")
843
+ col_a, col_b, col_c, col_d = st.columns(4)
844
+ generated_path: Path | None = None
845
+ if col_a.button("Generate baseline"):
846
+ generated_path = run_baseline(runs_dir)
847
+ if col_b.button("Generate optimized"):
848
+ generated_path = run_optimized(runs_dir)
849
+ if col_c.button("Generate LangGraph-style"):
850
+ generated_path = run_langgraph_style(runs_dir)
851
+ if col_d.button("Generate all"):
852
+ proof_baseline, proof_candidate = run_proof_pair(runs_dir)
853
+ for path in [
854
+ run_baseline(runs_dir),
855
+ run_optimized(runs_dir),
856
+ run_langgraph_style(runs_dir),
857
+ proof_baseline,
858
+ proof_candidate,
859
+ ]:
860
+ store.save_trace(AgentTrace.from_json(path), project_id=project_id)
861
+ st.success(f"Generated and stored all demo traces under project `{project_id}`")
862
+
863
+ if st.button("Generate proof demo pair"):
864
+ proof_baseline, proof_candidate = run_proof_pair(runs_dir)
865
+ for path in [proof_baseline, proof_candidate]:
866
+ store.save_trace(AgentTrace.from_json(path), project_id=project_id)
867
+ st.success(f"Generated and stored proof demo traces under project `{project_id}`")
868
+
869
+ if generated_path is not None:
870
+ trace = AgentTrace.from_json(generated_path)
871
+ store.save_trace(trace, project_id=project_id)
872
+ st.success(f"Generated and stored `{trace.name}` as `{trace.run_id}`")
873
+
874
+ uploaded = st.file_uploader("Upload AgentLoop trace JSON", type=["json"])
875
+ if uploaded is not None:
876
+ try:
877
+ trace = parse_uploaded_trace(uploaded.read())
878
+ except TraceUploadError as exc:
879
+ st.error(f"Fix the uploaded trace: {exc}.")
880
+ else:
881
+ if st.button("Store uploaded trace"):
882
+ store.save_trace(trace, project_id=project_id)
883
+ st.success(f"Stored uploaded trace `{trace.run_id}` under project `{project_id}`")
884
+
885
+ st.subheader("CLI ingest")
886
+ st.code(
887
+ "uv run agentloop demo-all\n"
888
+ f"uv run agentloop store-trace --path runs/research_agent_baseline.json --project-id {project_id}\n"
889
+ f"uv run agentloop list-stored-traces --project-id {project_id}",
890
+ language="bash",
891
+ )
892
+
893
+ elif page == "Setup":
894
+ st.subheader("Local development")
895
+ st.code(
896
+ "uv sync --locked --all-extras --dev\n"
897
+ "uv run agentloop init-store\n"
898
+ "uv run agentloop demo-all\n"
899
+ f"uv run agentloop store-trace --path runs/research_agent_baseline.json --project-id {project_id}\n"
900
+ "uv run streamlit run dashboard/app.py",
901
+ language="bash",
902
+ )
903
+
904
+ st.subheader("Hosted API")
905
+ st.code(
906
+ "uv run agentloop create-api-key --project-id acme --name local-dev\n"
907
+ "uv run agentloop server --host 127.0.0.1 --port 8000\n"
908
+ "uv run agentloop upload --path runs/research_agent_baseline.json "
909
+ "--api-url http://127.0.0.1:8000 --api-key al_xxx\n"
910
+ "uv run agentloop remote-usage --api-url http://127.0.0.1:8000 --api-key al_xxx",
911
+ language="bash",
912
+ )
913
+
914
+ st.subheader("Postgres deployment")
915
+ st.code(
916
+ "uv sync --locked --extra server --extra postgres --no-dev\n"
917
+ "export AGENTLOOP_STORE_BACKEND=postgres\n"
918
+ "export AGENTLOOP_DATABASE_URL=postgresql://agentloop:agentloop@localhost:5432/agentloop\n"
919
+ "uv run agentloop init-store\n"
920
+ "uv run agentloop server --host 0.0.0.0 --port 8000",
921
+ language="bash",
922
+ )