agentloop-profiler 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
agentloop/cli.py
ADDED
|
@@ -0,0 +1,1314 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from agentloop.ablation_protocol import AblationValidationError
|
|
12
|
+
from agentloop.ablations import ablation_to_markdown, summarize_ablation
|
|
13
|
+
from agentloop.aggregate_cli import aggregate_app
|
|
14
|
+
from agentloop.audit import estimate_improvement
|
|
15
|
+
from agentloop.autoinstrument import detect_integrations
|
|
16
|
+
from agentloop.calibration import (
|
|
17
|
+
CalibrationValidationError,
|
|
18
|
+
calibration_to_markdown,
|
|
19
|
+
summarize_calibration,
|
|
20
|
+
)
|
|
21
|
+
from agentloop.ci import build_ci_report, ci_report_to_markdown
|
|
22
|
+
from agentloop.client import AgentLoopClient, AgentLoopClientError
|
|
23
|
+
from agentloop.costs import format_cost_usd, is_cost_evaluable
|
|
24
|
+
from agentloop.demo import run_baseline, run_langgraph_style, run_optimized, run_proof_pair
|
|
25
|
+
from agentloop.doctor import run_doctor, run_production_check
|
|
26
|
+
from agentloop.drift_cli import drift_app
|
|
27
|
+
from agentloop.exporters import export_report_markdown
|
|
28
|
+
from agentloop.findings import build_diagnosis, diagnosis_to_markdown
|
|
29
|
+
from agentloop.harness_bench.cli import harness_app
|
|
30
|
+
from agentloop.integrations.harbor.cli import harbor_app
|
|
31
|
+
from agentloop.integrations.omnigent.cli import omnigent_app
|
|
32
|
+
from agentloop.interoperability.cli import telemetry_app
|
|
33
|
+
from agentloop.intervention_service import create_stored_intervention
|
|
34
|
+
from agentloop.interventions import (
|
|
35
|
+
InterventionConflictError,
|
|
36
|
+
InterventionReferenceError,
|
|
37
|
+
InterventionValidationError,
|
|
38
|
+
build_intervention,
|
|
39
|
+
)
|
|
40
|
+
from agentloop.issues import build_issue_drafts, issue_drafts_to_markdown
|
|
41
|
+
from agentloop.optimizer import build_optimization_plan
|
|
42
|
+
from agentloop.otel import trace_from_otel, trace_to_otel
|
|
43
|
+
from agentloop.patches import build_patch_plan, patch_plan_to_markdown
|
|
44
|
+
from agentloop.plan_export import export_optimization_json, export_optimization_markdown
|
|
45
|
+
from agentloop.quality import (
|
|
46
|
+
QualityValidationError,
|
|
47
|
+
build_quality_report,
|
|
48
|
+
load_quality_fixtures,
|
|
49
|
+
quality_report_to_markdown,
|
|
50
|
+
)
|
|
51
|
+
from agentloop.ranking import RankingSort, sort_ranked
|
|
52
|
+
from agentloop.replay import ReplayGates, build_replay_report, replay_report_to_markdown
|
|
53
|
+
from agentloop.store import (
|
|
54
|
+
DEFAULT_PAGE_SIZE,
|
|
55
|
+
FindingNotFoundError,
|
|
56
|
+
FindingTransitionError,
|
|
57
|
+
get_store,
|
|
58
|
+
)
|
|
59
|
+
from agentloop.studies import StudyValidationError, study_to_markdown, summarize_study
|
|
60
|
+
from agentloop.timing import format_duration_ms
|
|
61
|
+
from agentloop.tracer import AgentTrace
|
|
62
|
+
from agentloop.value import build_value_report
|
|
63
|
+
|
|
64
|
+
app = typer.Typer(help="AgentLoop profiler CLI")
|
|
65
|
+
console = Console()
|
|
66
|
+
study_app = typer.Typer(help="Summarize offline studies and preserved intervention evidence.")
|
|
67
|
+
app.add_typer(study_app, name="study")
|
|
68
|
+
app.add_typer(aggregate_app, name="aggregate")
|
|
69
|
+
app.add_typer(drift_app, name="drift")
|
|
70
|
+
app.add_typer(harbor_app, name="harbor")
|
|
71
|
+
app.add_typer(telemetry_app, name="telemetry")
|
|
72
|
+
app.add_typer(omnigent_app, name="omnigent")
|
|
73
|
+
app.add_typer(harness_app, name="harness")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command("onboard")
|
|
77
|
+
def onboard_command(
|
|
78
|
+
path: Path,
|
|
79
|
+
format: str = typer.Option(
|
|
80
|
+
"native", help="native or otlp; OTLP includes supported OpenInference attributes."
|
|
81
|
+
),
|
|
82
|
+
out: Path = typer.Option(
|
|
83
|
+
Path("runs/onboarding.json"), help="Validation and payload-free analysis summary."
|
|
84
|
+
),
|
|
85
|
+
expected_operation: list[str] | None = typer.Option(
|
|
86
|
+
None, help="Repeat for expected model/tool/classifier/etc. kinds."
|
|
87
|
+
),
|
|
88
|
+
analyze: bool = typer.Option(
|
|
89
|
+
True, help="Analyze after validation; limited to 2000 spans per trace."
|
|
90
|
+
),
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Validate captured evidence and analyze existing telemetry without application changes."""
|
|
93
|
+
from agentloop.aggregates import read_json
|
|
94
|
+
from agentloop.onboarding import onboard
|
|
95
|
+
|
|
96
|
+
if path.resolve() == out.resolve():
|
|
97
|
+
raise typer.BadParameter("output must differ from the source telemetry", param_hint="out")
|
|
98
|
+
try:
|
|
99
|
+
result = onboard(
|
|
100
|
+
read_json(path),
|
|
101
|
+
format=format,
|
|
102
|
+
expected_operations=tuple(expected_operation or ()),
|
|
103
|
+
analyze=analyze,
|
|
104
|
+
)
|
|
105
|
+
except (OSError, ValueError, TypeError, OverflowError) as exc:
|
|
106
|
+
raise typer.BadParameter(str(exc), param_hint="path/format") from None
|
|
107
|
+
_write_json(out, result)
|
|
108
|
+
console.print(
|
|
109
|
+
f"Capture status: {result['status']}; traces: {result['trace_count']}", markup=False
|
|
110
|
+
)
|
|
111
|
+
for item in result["traces"]:
|
|
112
|
+
console.print(
|
|
113
|
+
f"Trace {item['run_id']}: {item['validation']['status']}; analysis {item['analysis_status']}",
|
|
114
|
+
markup=False,
|
|
115
|
+
)
|
|
116
|
+
for check in item["validation"]["checks"]:
|
|
117
|
+
console.print(f"- {check['code']} ({check['count']}): {check['detail']}", markup=False)
|
|
118
|
+
if item["analysis"] is not None:
|
|
119
|
+
report = item["analysis"]
|
|
120
|
+
console.print(
|
|
121
|
+
f" Runtime {report['total_runtime_ms']} ms; model calls {report['model_call_count']}; token basis {report['token_status']}; findings {len(item['findings'])}",
|
|
122
|
+
markup=False,
|
|
123
|
+
)
|
|
124
|
+
console.print(f"Wrote onboarding evidence to {out}")
|
|
125
|
+
if result["status"] in {"invalid", "no_execution_data"}:
|
|
126
|
+
raise typer.Exit(code=1)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@study_app.command("ablation")
|
|
130
|
+
def study_ablation_command(
|
|
131
|
+
bundle: Path,
|
|
132
|
+
out: Path = typer.Option(Path("runs/ablation.md"), help="Readable ablation report."),
|
|
133
|
+
json_out: Path = typer.Option(
|
|
134
|
+
Path("runs/ablation.json"), help="Full observations and denominators."
|
|
135
|
+
),
|
|
136
|
+
) -> None:
|
|
137
|
+
try:
|
|
138
|
+
report = summarize_ablation(bundle)
|
|
139
|
+
except AblationValidationError as exc:
|
|
140
|
+
raise typer.BadParameter(str(exc), param_hint="bundle") from None
|
|
141
|
+
outputs = {out.resolve(), json_out.resolve()}
|
|
142
|
+
sources = {Path(source).resolve() for source in report["source_paths"]}
|
|
143
|
+
if len(outputs) != 2 or outputs & sources:
|
|
144
|
+
raise typer.BadParameter(
|
|
145
|
+
"report paths must differ from each other and all source evidence",
|
|
146
|
+
param_hint="out/json-out",
|
|
147
|
+
)
|
|
148
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
out.write_text(ablation_to_markdown(report), encoding="utf-8")
|
|
150
|
+
_write_json(json_out, report)
|
|
151
|
+
console.print(f"Wrote ablation reports to {out} and {json_out}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@study_app.command("calibrate")
|
|
155
|
+
def study_calibrate_command(
|
|
156
|
+
manifest: Path,
|
|
157
|
+
out: Path = typer.Option(Path("runs/calibration.md"), help="Readable historical calibration."),
|
|
158
|
+
json_out: Path = typer.Option(
|
|
159
|
+
Path("runs/calibration.json"), help="Versioned calibration artifact."
|
|
160
|
+
),
|
|
161
|
+
) -> None:
|
|
162
|
+
try:
|
|
163
|
+
report = summarize_calibration(manifest)
|
|
164
|
+
except CalibrationValidationError as exc:
|
|
165
|
+
raise typer.BadParameter(str(exc), param_hint="manifest") from None
|
|
166
|
+
outputs = {out.resolve(), json_out.resolve()}
|
|
167
|
+
sources = {Path(source).resolve() for source in report["source_paths"]}
|
|
168
|
+
if len(outputs) != 2 or outputs & sources:
|
|
169
|
+
raise typer.BadParameter(
|
|
170
|
+
"report paths must differ from each other and all source evidence",
|
|
171
|
+
param_hint="out/json-out",
|
|
172
|
+
)
|
|
173
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
out.write_text(calibration_to_markdown(report), encoding="utf-8")
|
|
175
|
+
_write_json(json_out, report)
|
|
176
|
+
console.print(f"Wrote calibration reports to {out} and {json_out}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@study_app.command("summarize")
|
|
180
|
+
def study_summarize_command(
|
|
181
|
+
manifest: Path,
|
|
182
|
+
out: Path = typer.Option(Path("runs/study.md"), help="Human-readable study report."),
|
|
183
|
+
json_out: Path = typer.Option(
|
|
184
|
+
Path("runs/study.json"), help="Full study results and unmatched runs."
|
|
185
|
+
),
|
|
186
|
+
) -> None:
|
|
187
|
+
try:
|
|
188
|
+
report = summarize_study(manifest)
|
|
189
|
+
except StudyValidationError as exc:
|
|
190
|
+
raise typer.BadParameter(str(exc), param_hint="manifest") from None
|
|
191
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
out.write_text(study_to_markdown(report), encoding="utf-8")
|
|
193
|
+
_write_json(json_out, report)
|
|
194
|
+
console.print(f"Wrote study reports to {out} and {json_out}")
|
|
195
|
+
for name, comparison in report["comparisons"].items():
|
|
196
|
+
console.print(
|
|
197
|
+
f"{name}: {comparison['pair_count']} paired runs, {len(comparison['unmatched'])} unmatched runs"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _remote_client(api_url: str, api_key: str | None) -> AgentLoopClient:
|
|
202
|
+
resolved_api_key = api_key if api_key is not None else os.getenv("AGENTLOOP_API_KEY")
|
|
203
|
+
return AgentLoopClient(base_url=api_url, api_key=resolved_api_key)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _remote_admin_client(api_url: str, admin_api_key: str | None) -> AgentLoopClient:
|
|
207
|
+
resolved_admin_key = (
|
|
208
|
+
admin_api_key if admin_api_key is not None else os.getenv("AGENTLOOP_ADMIN_API_KEY")
|
|
209
|
+
)
|
|
210
|
+
return AgentLoopClient(base_url=api_url, admin_api_key=resolved_admin_key)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _read_json_input(path: Path, *, param_hint: str) -> object:
|
|
214
|
+
try:
|
|
215
|
+
exists = path.exists()
|
|
216
|
+
is_file = path.is_file() if exists else False
|
|
217
|
+
except OSError as exc:
|
|
218
|
+
raise typer.BadParameter(
|
|
219
|
+
f"Input file is not readable: {path} ({exc})", param_hint=param_hint
|
|
220
|
+
) from exc
|
|
221
|
+
|
|
222
|
+
if not exists:
|
|
223
|
+
raise typer.BadParameter(
|
|
224
|
+
f"Input file does not exist: {path}. "
|
|
225
|
+
"Generate synthetic traces explicitly with `agentloop demo` or `agentloop demo-all`.",
|
|
226
|
+
param_hint=param_hint,
|
|
227
|
+
)
|
|
228
|
+
if not is_file:
|
|
229
|
+
raise typer.BadParameter(f"Input path is not a regular file: {path}", param_hint=param_hint)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
raw = path.read_text(encoding="utf-8")
|
|
233
|
+
except (OSError, UnicodeError) as exc:
|
|
234
|
+
raise typer.BadParameter(
|
|
235
|
+
f"Input file is not readable as UTF-8: {path} ({exc})", param_hint=param_hint
|
|
236
|
+
) from exc
|
|
237
|
+
try:
|
|
238
|
+
return json.loads(raw)
|
|
239
|
+
except json.JSONDecodeError as exc:
|
|
240
|
+
raise typer.BadParameter(
|
|
241
|
+
f"Input file is not valid JSON: {path} ({exc.msg} at line {exc.lineno}, "
|
|
242
|
+
f"column {exc.colno})",
|
|
243
|
+
param_hint=param_hint,
|
|
244
|
+
) from exc
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _write_json(out: Path, payload: dict) -> None:
|
|
248
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
249
|
+
out.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _load_trace(
|
|
253
|
+
path: Path,
|
|
254
|
+
*,
|
|
255
|
+
otel: bool = False,
|
|
256
|
+
name: str | None = None,
|
|
257
|
+
param_hint: str = "path",
|
|
258
|
+
) -> AgentTrace:
|
|
259
|
+
payload = _read_json_input(path, param_hint=param_hint)
|
|
260
|
+
format_name = "OpenTelemetry" if otel else "AgentLoop"
|
|
261
|
+
try:
|
|
262
|
+
if otel:
|
|
263
|
+
if not isinstance(payload, (dict, list)):
|
|
264
|
+
raise ValueError("expected a JSON object or array")
|
|
265
|
+
return trace_from_otel(payload, name=name)
|
|
266
|
+
if not isinstance(payload, dict):
|
|
267
|
+
raise ValueError("expected a JSON object")
|
|
268
|
+
return AgentTrace.from_dict(payload)
|
|
269
|
+
except (AttributeError, KeyError, TypeError, ValueError) as exc:
|
|
270
|
+
raise typer.BadParameter(
|
|
271
|
+
f"Input file is not a valid {format_name} trace: {path} ({exc})",
|
|
272
|
+
param_hint=param_hint,
|
|
273
|
+
) from exc
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _build_quality_report_from_file(
|
|
277
|
+
path: Path,
|
|
278
|
+
*,
|
|
279
|
+
param_hint: str,
|
|
280
|
+
baseline_trace: AgentTrace | None = None,
|
|
281
|
+
candidate_trace: AgentTrace | None = None,
|
|
282
|
+
min_score: float | None = None,
|
|
283
|
+
) -> dict:
|
|
284
|
+
try:
|
|
285
|
+
fixtures = load_quality_fixtures(path)
|
|
286
|
+
return build_quality_report(
|
|
287
|
+
fixtures,
|
|
288
|
+
baseline_trace=baseline_trace,
|
|
289
|
+
candidate_trace=candidate_trace,
|
|
290
|
+
min_score=min_score,
|
|
291
|
+
)
|
|
292
|
+
except (OSError, UnicodeError, QualityValidationError) as exc:
|
|
293
|
+
raise typer.BadParameter(
|
|
294
|
+
f"Invalid quality fixture file: {path} ({exc})",
|
|
295
|
+
param_hint=param_hint,
|
|
296
|
+
) from exc
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _print_value_summary(value: dict) -> None:
|
|
300
|
+
monthly = value["monthly_value"]
|
|
301
|
+
per_run = value["per_run"]
|
|
302
|
+
reliability = value["reliability"]
|
|
303
|
+
pricing = value.get("pricing", {})
|
|
304
|
+
table = Table(title="AgentLoop Value Report")
|
|
305
|
+
table.add_column("Metric")
|
|
306
|
+
table.add_column("Value")
|
|
307
|
+
table.add_row(
|
|
308
|
+
"Total monthly value", format_cost_usd(monthly.get("total_value_usd"), decimals=2)
|
|
309
|
+
)
|
|
310
|
+
table.add_row(
|
|
311
|
+
"Model cost savings / month",
|
|
312
|
+
format_cost_usd(monthly.get("direct_model_cost_savings_usd"), decimals=2),
|
|
313
|
+
)
|
|
314
|
+
table.add_row("Engineering hours saved / month", str(monthly["engineering_hours_saved"]))
|
|
315
|
+
table.add_row("Latency saved / run", f"{per_run['latency_savings_ms'] / 1000:.3f}s")
|
|
316
|
+
table.add_row("Cost saved / run", format_cost_usd(per_run.get("cost_savings_usd"), decimals=6))
|
|
317
|
+
table.add_row("Reliability risk score", f"{reliability['risk_score']}/100")
|
|
318
|
+
if pricing:
|
|
319
|
+
table.add_row("Suggested plan", str(pricing.get("suggested_plan") or "unavailable"))
|
|
320
|
+
table.add_row(
|
|
321
|
+
"Suggested monthly price",
|
|
322
|
+
format_cost_usd(pricing.get("suggested_monthly_price_usd"), decimals=0),
|
|
323
|
+
)
|
|
324
|
+
ratio = pricing.get("value_to_price_ratio")
|
|
325
|
+
table.add_row("Value / price ratio", "n/a" if ratio is None else f"{ratio}x")
|
|
326
|
+
console.print(table)
|
|
327
|
+
console.print(value["value_summary"])
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _print_doctor(result: dict) -> None:
|
|
331
|
+
table = Table(title="AgentLoop Doctor")
|
|
332
|
+
table.add_column("Status")
|
|
333
|
+
table.add_column("Check")
|
|
334
|
+
table.add_column("Detail")
|
|
335
|
+
table.add_column("Fix")
|
|
336
|
+
for check in result["checks"]:
|
|
337
|
+
status = check["status"]
|
|
338
|
+
style = "green" if status == "ok" else "yellow" if status == "warn" else "red"
|
|
339
|
+
table.add_row(
|
|
340
|
+
f"[{style}]{status}[/{style}]", check["name"], check["detail"], check.get("fix", "")
|
|
341
|
+
)
|
|
342
|
+
console.print(table)
|
|
343
|
+
if result["failed"]:
|
|
344
|
+
raise typer.Exit(1)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@app.command()
|
|
348
|
+
def report(path: Path) -> None:
|
|
349
|
+
trace = _load_trace(path)
|
|
350
|
+
trace.print_report()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@app.command()
|
|
354
|
+
def compare(
|
|
355
|
+
baseline: Path = Path("runs/research_agent_baseline.json"),
|
|
356
|
+
optimized: Path = Path("runs/research_agent_optimized.json"),
|
|
357
|
+
) -> None:
|
|
358
|
+
base_trace = _load_trace(baseline, param_hint="--baseline")
|
|
359
|
+
opt_trace = _load_trace(optimized, param_hint="--optimized")
|
|
360
|
+
from agentloop.interoperability.evidence import require_external_comparison
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
require_external_comparison(base_trace, "compare")
|
|
364
|
+
require_external_comparison(opt_trace, "compare")
|
|
365
|
+
except ValueError as exc:
|
|
366
|
+
raise typer.BadParameter(str(exc), param_hint="--baseline/--optimized") from None
|
|
367
|
+
base = base_trace.report()
|
|
368
|
+
opt = opt_trace.report()
|
|
369
|
+
table = Table(title="AgentLoop Comparison")
|
|
370
|
+
table.add_column("Metric")
|
|
371
|
+
table.add_column("Baseline")
|
|
372
|
+
table.add_column("Optimized")
|
|
373
|
+
table.add_column("Delta")
|
|
374
|
+
|
|
375
|
+
pairs = [
|
|
376
|
+
("Runtime sec", base["total_runtime_ms"] / 1000, opt["total_runtime_ms"] / 1000),
|
|
377
|
+
("Cost USD", base["estimated_cost_usd"], opt["estimated_cost_usd"]),
|
|
378
|
+
("Input tokens", base["input_tokens"], opt["input_tokens"]),
|
|
379
|
+
("Retry count", base["retry_count"], opt["retry_count"]),
|
|
380
|
+
]
|
|
381
|
+
for name, b, o in pairs:
|
|
382
|
+
if name == "Cost USD" and not (
|
|
383
|
+
is_cost_evaluable(base.get("cost_status", "complete"))
|
|
384
|
+
and is_cost_evaluable(opt.get("cost_status", "complete"))
|
|
385
|
+
):
|
|
386
|
+
table.add_row(
|
|
387
|
+
name,
|
|
388
|
+
format_cost_usd(b, base.get("cost_status")),
|
|
389
|
+
format_cost_usd(o, opt.get("cost_status")),
|
|
390
|
+
"unavailable",
|
|
391
|
+
)
|
|
392
|
+
continue
|
|
393
|
+
table.add_row(
|
|
394
|
+
name,
|
|
395
|
+
f"{b:.4f}" if isinstance(b, float) else str(b),
|
|
396
|
+
f"{o:.4f}" if isinstance(o, float) else str(o),
|
|
397
|
+
f"{(o - b):.4f}" if isinstance(o, float) else str(o - b),
|
|
398
|
+
)
|
|
399
|
+
console.print(table)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@app.command("replay")
|
|
403
|
+
def replay_command(
|
|
404
|
+
baseline: Path = Path("runs/research_agent_baseline.json"),
|
|
405
|
+
candidate: Path = Path("runs/research_agent_optimized.json"),
|
|
406
|
+
out: Path = Path("runs/replay_report.md"),
|
|
407
|
+
json_out: Path | None = typer.Option(
|
|
408
|
+
None, help="Optional machine-readable replay report output."
|
|
409
|
+
),
|
|
410
|
+
max_cost_regression_pct: float = typer.Option(0.0, min=0.0),
|
|
411
|
+
max_latency_regression_pct: float = typer.Option(0.0, min=0.0),
|
|
412
|
+
min_latency_improvement_pct: float = typer.Option(0.0, min=0.0),
|
|
413
|
+
min_cost_improvement_pct: float = typer.Option(0.0, min=0.0),
|
|
414
|
+
require_retry_non_increase: bool = typer.Option(True),
|
|
415
|
+
require_schema_valid: bool = typer.Option(
|
|
416
|
+
False, help="Require candidate trace metadata/report to mark schema output as valid."
|
|
417
|
+
),
|
|
418
|
+
min_quality_score: float | None = typer.Option(
|
|
419
|
+
None, min=0.0, max=1.0, help="Optional minimum candidate quality score."
|
|
420
|
+
),
|
|
421
|
+
quality_fixtures: Path | None = typer.Option(None, help="Optional quality fixture JSON file."),
|
|
422
|
+
fail_on_gate: bool = typer.Option(True, help="Exit non-zero when replay gates fail."),
|
|
423
|
+
intervention_out: Path | None = typer.Option(
|
|
424
|
+
None, help="Export linked intervention evidence, including failed gates."
|
|
425
|
+
),
|
|
426
|
+
target_finding: list[str] | None = typer.Option(
|
|
427
|
+
None, help="Baseline finding ID; repeat for multiple targets."
|
|
428
|
+
),
|
|
429
|
+
intervention_type: str | None = typer.Option(
|
|
430
|
+
None, help="Applied change, such as context_compression."
|
|
431
|
+
),
|
|
432
|
+
intervention_config: Path | None = typer.Option(
|
|
433
|
+
None, help="JSON object describing the applied change."
|
|
434
|
+
),
|
|
435
|
+
intervention_metadata: Path | None = typer.Option(
|
|
436
|
+
None, help="JSON object with task, seed, or experiment metadata."
|
|
437
|
+
),
|
|
438
|
+
baseline_diagnosis: Path | None = typer.Option(
|
|
439
|
+
None, help="Optional saved baseline diagnosis for historical prediction snapshots."
|
|
440
|
+
),
|
|
441
|
+
) -> None:
|
|
442
|
+
baseline_trace = _load_trace(baseline, param_hint="--baseline")
|
|
443
|
+
candidate_trace = _load_trace(candidate, param_hint="--candidate")
|
|
444
|
+
quality = (
|
|
445
|
+
_build_quality_report_from_file(
|
|
446
|
+
quality_fixtures,
|
|
447
|
+
param_hint="--quality-fixtures",
|
|
448
|
+
baseline_trace=baseline_trace,
|
|
449
|
+
candidate_trace=candidate_trace,
|
|
450
|
+
min_score=min_quality_score,
|
|
451
|
+
)
|
|
452
|
+
if quality_fixtures is not None
|
|
453
|
+
else None
|
|
454
|
+
)
|
|
455
|
+
report_data = build_replay_report(
|
|
456
|
+
baseline_trace,
|
|
457
|
+
candidate_trace,
|
|
458
|
+
gates=ReplayGates(
|
|
459
|
+
max_cost_regression_pct=max_cost_regression_pct,
|
|
460
|
+
max_latency_regression_pct=max_latency_regression_pct,
|
|
461
|
+
min_latency_improvement_pct=min_latency_improvement_pct,
|
|
462
|
+
min_cost_improvement_pct=min_cost_improvement_pct,
|
|
463
|
+
require_retry_non_increase=require_retry_non_increase,
|
|
464
|
+
require_schema_valid=require_schema_valid,
|
|
465
|
+
min_quality_score=min_quality_score,
|
|
466
|
+
),
|
|
467
|
+
quality_report=quality,
|
|
468
|
+
)
|
|
469
|
+
intervention = None
|
|
470
|
+
if intervention_out is not None:
|
|
471
|
+
try:
|
|
472
|
+
intervention = build_intervention(
|
|
473
|
+
baseline_trace,
|
|
474
|
+
candidate_trace,
|
|
475
|
+
target_finding_ids=target_finding or [],
|
|
476
|
+
intervention_type=intervention_type,
|
|
477
|
+
configuration=_json_object_input(intervention_config, "--intervention-config"),
|
|
478
|
+
metadata=_json_object_input(intervention_metadata, "--intervention-metadata"),
|
|
479
|
+
diagnosis=_json_object_input(baseline_diagnosis, "--baseline-diagnosis")
|
|
480
|
+
if baseline_diagnosis
|
|
481
|
+
else None,
|
|
482
|
+
replay_report=report_data,
|
|
483
|
+
).to_dict()
|
|
484
|
+
except (InterventionValidationError, InterventionReferenceError) as exc:
|
|
485
|
+
raise typer.BadParameter(str(exc), param_hint="--intervention-out") from None
|
|
486
|
+
elif any(
|
|
487
|
+
(
|
|
488
|
+
target_finding,
|
|
489
|
+
intervention_type,
|
|
490
|
+
intervention_config,
|
|
491
|
+
intervention_metadata,
|
|
492
|
+
baseline_diagnosis,
|
|
493
|
+
)
|
|
494
|
+
):
|
|
495
|
+
raise typer.BadParameter("intervention options require --intervention-out")
|
|
496
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
497
|
+
out.write_text(replay_report_to_markdown(report_data), encoding="utf-8")
|
|
498
|
+
if intervention is not None:
|
|
499
|
+
_write_json(intervention_out, intervention)
|
|
500
|
+
if json_out is not None:
|
|
501
|
+
_write_json(json_out, report_data)
|
|
502
|
+
console.print(f"Wrote replay report to {out}")
|
|
503
|
+
if json_out is not None:
|
|
504
|
+
console.print(f"Wrote replay JSON to {json_out}")
|
|
505
|
+
console.print(report_data["summary"])
|
|
506
|
+
if fail_on_gate and not report_data["gates"]["passed"]:
|
|
507
|
+
raise typer.Exit(1)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _json_object_input(path: Path | None, param_hint: str) -> dict:
|
|
511
|
+
if path is None:
|
|
512
|
+
return {}
|
|
513
|
+
payload = _read_json_input(path, param_hint=param_hint)
|
|
514
|
+
if not isinstance(payload, dict):
|
|
515
|
+
raise typer.BadParameter("expected a JSON object", param_hint=param_hint)
|
|
516
|
+
return payload
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@app.command("intervention-create")
|
|
520
|
+
def intervention_create_command(
|
|
521
|
+
request: Path,
|
|
522
|
+
out: Path = typer.Option(
|
|
523
|
+
Path("runs/intervention.json"), help="Export the stored evidence as JSON."
|
|
524
|
+
),
|
|
525
|
+
project_id: str = typer.Option(
|
|
526
|
+
"default", help="Project for a local store; remote API keys determine their project."
|
|
527
|
+
),
|
|
528
|
+
api_url: str | None = typer.Option(
|
|
529
|
+
None, help="Use a remote API instead of the configured local store."
|
|
530
|
+
),
|
|
531
|
+
api_key: str | None = typer.Option(
|
|
532
|
+
None, help="Remote project API key; defaults to AGENTLOOP_API_KEY."
|
|
533
|
+
),
|
|
534
|
+
) -> None:
|
|
535
|
+
payload = _json_object_input(request, "request")
|
|
536
|
+
try:
|
|
537
|
+
record = (
|
|
538
|
+
_remote_client(api_url, api_key).create_intervention(payload)
|
|
539
|
+
if api_url
|
|
540
|
+
else create_stored_intervention(get_store(), payload, project_id)
|
|
541
|
+
)
|
|
542
|
+
except (
|
|
543
|
+
InterventionValidationError,
|
|
544
|
+
InterventionReferenceError,
|
|
545
|
+
InterventionConflictError,
|
|
546
|
+
QualityValidationError,
|
|
547
|
+
AgentLoopClientError,
|
|
548
|
+
) as exc:
|
|
549
|
+
raise typer.BadParameter(str(exc), param_hint="request") from None
|
|
550
|
+
_write_json(out, record)
|
|
551
|
+
console.print(f"Wrote intervention {record['intervention_id']} to {out}")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@app.command("intervention-get")
|
|
555
|
+
def intervention_get_command(
|
|
556
|
+
intervention_id: str,
|
|
557
|
+
out: Path = typer.Option(Path("runs/intervention.json")),
|
|
558
|
+
project_id: str = typer.Option(
|
|
559
|
+
"default", help="Local project; remote API keys determine their project."
|
|
560
|
+
),
|
|
561
|
+
api_url: str | None = typer.Option(None),
|
|
562
|
+
api_key: str | None = typer.Option(None),
|
|
563
|
+
) -> None:
|
|
564
|
+
try:
|
|
565
|
+
record = (
|
|
566
|
+
_remote_client(api_url, api_key).get_intervention(intervention_id)
|
|
567
|
+
if api_url
|
|
568
|
+
else get_store().get_intervention(intervention_id, project_id=project_id)
|
|
569
|
+
)
|
|
570
|
+
except AgentLoopClientError as exc:
|
|
571
|
+
raise typer.BadParameter(str(exc), param_hint="intervention_id") from None
|
|
572
|
+
if record is None:
|
|
573
|
+
raise typer.BadParameter("intervention not found", param_hint="intervention_id")
|
|
574
|
+
_write_json(out, record)
|
|
575
|
+
console.print(f"Wrote intervention to {out}")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@app.command("quality-report")
|
|
579
|
+
def quality_report_command(
|
|
580
|
+
fixtures: Path,
|
|
581
|
+
baseline: Path | None = typer.Option(None, help="Optional baseline trace JSON."),
|
|
582
|
+
candidate: Path | None = typer.Option(None, help="Optional candidate trace JSON."),
|
|
583
|
+
out: Path = Path("runs/quality_report.md"),
|
|
584
|
+
json_out: Path | None = typer.Option(
|
|
585
|
+
None, help="Optional machine-readable quality report output."
|
|
586
|
+
),
|
|
587
|
+
min_score: float | None = typer.Option(None, min=0.0, max=1.0),
|
|
588
|
+
) -> None:
|
|
589
|
+
baseline_trace = (
|
|
590
|
+
_load_trace(baseline, param_hint="--baseline") if baseline is not None else None
|
|
591
|
+
)
|
|
592
|
+
candidate_trace = (
|
|
593
|
+
_load_trace(candidate, param_hint="--candidate") if candidate is not None else None
|
|
594
|
+
)
|
|
595
|
+
report_data = _build_quality_report_from_file(
|
|
596
|
+
fixtures,
|
|
597
|
+
param_hint="fixtures",
|
|
598
|
+
baseline_trace=baseline_trace,
|
|
599
|
+
candidate_trace=candidate_trace,
|
|
600
|
+
min_score=min_score,
|
|
601
|
+
)
|
|
602
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
603
|
+
out.write_text(quality_report_to_markdown(report_data), encoding="utf-8")
|
|
604
|
+
if json_out is not None:
|
|
605
|
+
_write_json(json_out, report_data)
|
|
606
|
+
console.print(f"Wrote quality JSON to {json_out}")
|
|
607
|
+
console.print(f"Wrote quality report to {out}")
|
|
608
|
+
if not report_data["passed"]:
|
|
609
|
+
raise typer.Exit(1)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@app.command("ci")
|
|
613
|
+
def ci_command(
|
|
614
|
+
baseline: Path = Path("runs/research_agent_baseline.json"),
|
|
615
|
+
candidate: Path = Path("runs/research_agent_optimized.json"),
|
|
616
|
+
out: Path = Path("runs/agentloop_ci.md"),
|
|
617
|
+
json_out: Path | None = typer.Option(None, help="Optional machine-readable CI report output."),
|
|
618
|
+
max_cost_regression_pct: float = typer.Option(0.0, min=0.0),
|
|
619
|
+
max_latency_regression_pct: float = typer.Option(0.0, min=0.0),
|
|
620
|
+
min_latency_improvement_pct: float = typer.Option(0.0, min=0.0),
|
|
621
|
+
min_cost_improvement_pct: float = typer.Option(0.0, min=0.0),
|
|
622
|
+
require_retry_non_increase: bool = typer.Option(True),
|
|
623
|
+
require_schema_valid: bool = typer.Option(
|
|
624
|
+
False, help="Require candidate trace metadata/report to mark schema output as valid."
|
|
625
|
+
),
|
|
626
|
+
min_quality_score: float | None = typer.Option(
|
|
627
|
+
None, min=0.0, max=1.0, help="Optional minimum candidate quality score."
|
|
628
|
+
),
|
|
629
|
+
quality_fixtures: Path | None = typer.Option(None, help="Optional quality fixture JSON file."),
|
|
630
|
+
github_step_summary: bool = typer.Option(
|
|
631
|
+
False, help="Append report Markdown to GITHUB_STEP_SUMMARY."
|
|
632
|
+
),
|
|
633
|
+
fail_on_gate: bool = typer.Option(True, help="Exit non-zero when CI gates fail."),
|
|
634
|
+
) -> None:
|
|
635
|
+
baseline_trace = _load_trace(baseline, param_hint="--baseline")
|
|
636
|
+
candidate_trace = _load_trace(candidate, param_hint="--candidate")
|
|
637
|
+
quality = (
|
|
638
|
+
_build_quality_report_from_file(
|
|
639
|
+
quality_fixtures,
|
|
640
|
+
param_hint="--quality-fixtures",
|
|
641
|
+
baseline_trace=baseline_trace,
|
|
642
|
+
candidate_trace=candidate_trace,
|
|
643
|
+
min_score=min_quality_score,
|
|
644
|
+
)
|
|
645
|
+
if quality_fixtures is not None
|
|
646
|
+
else None
|
|
647
|
+
)
|
|
648
|
+
report_data = build_ci_report(
|
|
649
|
+
baseline_trace,
|
|
650
|
+
candidate_trace,
|
|
651
|
+
gates=ReplayGates(
|
|
652
|
+
max_cost_regression_pct=max_cost_regression_pct,
|
|
653
|
+
max_latency_regression_pct=max_latency_regression_pct,
|
|
654
|
+
min_latency_improvement_pct=min_latency_improvement_pct,
|
|
655
|
+
min_cost_improvement_pct=min_cost_improvement_pct,
|
|
656
|
+
require_retry_non_increase=require_retry_non_increase,
|
|
657
|
+
require_schema_valid=require_schema_valid,
|
|
658
|
+
min_quality_score=min_quality_score,
|
|
659
|
+
),
|
|
660
|
+
quality_report=quality,
|
|
661
|
+
)
|
|
662
|
+
report_data["trace_inputs"]["baseline"]["path"] = str(baseline)
|
|
663
|
+
report_data["trace_inputs"]["candidate"]["path"] = str(candidate)
|
|
664
|
+
markdown = ci_report_to_markdown(report_data)
|
|
665
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
666
|
+
out.write_text(markdown, encoding="utf-8")
|
|
667
|
+
if json_out is not None:
|
|
668
|
+
_write_json(json_out, report_data)
|
|
669
|
+
if github_step_summary:
|
|
670
|
+
summary_path = os.getenv("GITHUB_STEP_SUMMARY")
|
|
671
|
+
if summary_path:
|
|
672
|
+
with open(summary_path, "a", encoding="utf-8") as handle:
|
|
673
|
+
handle.write(markdown)
|
|
674
|
+
console.print(f"Wrote CI report to {out}")
|
|
675
|
+
if json_out is not None:
|
|
676
|
+
console.print(f"Wrote CI JSON to {json_out}")
|
|
677
|
+
console.print(report_data["replay"]["summary"])
|
|
678
|
+
if fail_on_gate and not report_data["passed"]:
|
|
679
|
+
raise typer.Exit(1)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@app.command("dump-report")
|
|
683
|
+
def dump_report(path: Path, out: Path) -> None:
|
|
684
|
+
trace = _load_trace(path)
|
|
685
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
686
|
+
out.write_text(json.dumps(trace.report(), indent=2), encoding="utf-8")
|
|
687
|
+
console.print(f"Wrote {out}")
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
@app.command()
|
|
691
|
+
def audit(
|
|
692
|
+
path: Path = Path("runs/research_agent_baseline.json"),
|
|
693
|
+
out: Path = Path("runs/agentloop_audit.md"),
|
|
694
|
+
) -> None:
|
|
695
|
+
trace = _load_trace(path, param_hint="--path")
|
|
696
|
+
report_data = trace.report()
|
|
697
|
+
improvement = estimate_improvement(report_data)
|
|
698
|
+
report_data["estimated_latency_savings_ms"] = improvement["estimated_latency_savings_ms"]
|
|
699
|
+
report_data["estimated_cost_savings_usd"] = improvement["estimated_cost_savings_usd"]
|
|
700
|
+
export_report_markdown(report_data, out)
|
|
701
|
+
console.print(f"Wrote audit to {out}")
|
|
702
|
+
console.print(
|
|
703
|
+
f"Estimated savings: {improvement['estimated_latency_savings_ms'] / 1000:.2f}s latency, "
|
|
704
|
+
f"{format_cost_usd(improvement['estimated_cost_savings_usd'])} cost per run"
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
@app.command()
|
|
709
|
+
def optimize(
|
|
710
|
+
path: Path = Path("runs/research_agent_baseline.json"),
|
|
711
|
+
out: Path = Path("runs/optimization_plan.md"),
|
|
712
|
+
json_out: Path | None = typer.Option(None, help="Optional JSON output path."),
|
|
713
|
+
) -> None:
|
|
714
|
+
trace = _load_trace(path, param_hint="--path")
|
|
715
|
+
plan = build_optimization_plan(trace)
|
|
716
|
+
export_optimization_markdown(plan, out)
|
|
717
|
+
if json_out is not None:
|
|
718
|
+
export_optimization_json(plan, json_out)
|
|
719
|
+
console.print(f"Wrote optimization plan to {out}")
|
|
720
|
+
if json_out is not None:
|
|
721
|
+
console.print(f"Wrote optimization JSON to {json_out}")
|
|
722
|
+
after = plan["estimated_after"]
|
|
723
|
+
console.print(
|
|
724
|
+
"Estimated improvement: "
|
|
725
|
+
+ (
|
|
726
|
+
"unavailable latency, "
|
|
727
|
+
if after["latency_reduction_pct"] is None
|
|
728
|
+
else f"{after['latency_reduction_pct']:.1f}% latency, "
|
|
729
|
+
)
|
|
730
|
+
+ (
|
|
731
|
+
"unavailable cost"
|
|
732
|
+
if after["cost_reduction_pct"] is None
|
|
733
|
+
else f"{after['cost_reduction_pct']:.1f}% cost"
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@app.command("diagnose")
|
|
739
|
+
def diagnose(
|
|
740
|
+
path: Path = Path("runs/research_agent_baseline.json"),
|
|
741
|
+
out: Path = Path("runs/diagnosis.md"),
|
|
742
|
+
json_out: Path | None = typer.Option(None, help="Optional machine-readable diagnosis output."),
|
|
743
|
+
otel: bool = typer.Option(False, help="Read the input path as OTLP/GenAI-style JSON."),
|
|
744
|
+
name: str | None = typer.Option(None, help="Trace name to use when importing OTLP JSON."),
|
|
745
|
+
sort_by: RankingSort = typer.Option(
|
|
746
|
+
RankingSort.PRIORITY, help="Finding ranking dimension; unknown values stay explicit."
|
|
747
|
+
),
|
|
748
|
+
) -> None:
|
|
749
|
+
trace = _load_trace(path, otel=otel, name=name, param_hint="--path")
|
|
750
|
+
diagnosis = build_diagnosis(trace, sort_by=sort_by.value)
|
|
751
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
752
|
+
out.write_text(diagnosis_to_markdown(diagnosis), encoding="utf-8")
|
|
753
|
+
if json_out is not None:
|
|
754
|
+
_write_json(json_out, diagnosis)
|
|
755
|
+
summary = diagnosis["summary"]
|
|
756
|
+
console.print(f"Wrote diagnosis to {out}")
|
|
757
|
+
if json_out is not None:
|
|
758
|
+
console.print(f"Wrote diagnosis JSON to {json_out}")
|
|
759
|
+
console.print(
|
|
760
|
+
f"Findings: {summary['finding_count']} "
|
|
761
|
+
f"({summary['patchable_count']} patchable), "
|
|
762
|
+
"estimated improvement: "
|
|
763
|
+
+ (
|
|
764
|
+
"unavailable latency, "
|
|
765
|
+
if summary["estimated_latency_reduction_pct"] is None
|
|
766
|
+
else f"{summary['estimated_latency_reduction_pct']:.1f}% latency, "
|
|
767
|
+
)
|
|
768
|
+
+ (
|
|
769
|
+
"unavailable cost"
|
|
770
|
+
if summary["estimated_cost_reduction_pct"] is None
|
|
771
|
+
else f"{summary['estimated_cost_reduction_pct']:.1f}% cost"
|
|
772
|
+
)
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
@app.command("import-otel")
|
|
777
|
+
def import_otel(
|
|
778
|
+
path: Path,
|
|
779
|
+
out: Path,
|
|
780
|
+
name: str | None = typer.Option(None, help="Optional imported trace name."),
|
|
781
|
+
) -> None:
|
|
782
|
+
trace = _load_trace(path, otel=True, name=name)
|
|
783
|
+
trace.export_json(out)
|
|
784
|
+
console.print(f"Imported OTLP trace {trace.run_id} to {out}")
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
@app.command("export-otel")
|
|
788
|
+
def export_otel(path: Path, out: Path) -> None:
|
|
789
|
+
trace = _load_trace(path)
|
|
790
|
+
_write_json(out, trace_to_otel(trace))
|
|
791
|
+
console.print(f"Exported OTLP-style trace to {out}")
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@app.command("patch")
|
|
795
|
+
def patch_command(
|
|
796
|
+
path: Path = Path("runs/research_agent_baseline.json"),
|
|
797
|
+
repo: Path = Path("."),
|
|
798
|
+
out: Path = Path("runs/patch_plan.md"),
|
|
799
|
+
json_out: Path | None = typer.Option(None, help="Optional machine-readable patch plan output."),
|
|
800
|
+
otel: bool = typer.Option(False, help="Read the input path as OTLP/GenAI-style JSON."),
|
|
801
|
+
name: str | None = typer.Option(None, help="Trace name to use when importing OTLP JSON."),
|
|
802
|
+
dry_run: bool = typer.Option(
|
|
803
|
+
True, help="Only generate a patch plan. File edits are not supported yet."
|
|
804
|
+
),
|
|
805
|
+
) -> None:
|
|
806
|
+
if not dry_run:
|
|
807
|
+
raise typer.BadParameter(
|
|
808
|
+
"Only --dry-run is supported. Generate a patch plan first, then apply manually."
|
|
809
|
+
)
|
|
810
|
+
trace = _load_trace(path, otel=otel, name=name, param_hint="--path")
|
|
811
|
+
try:
|
|
812
|
+
plan = build_patch_plan(trace, repo_path=repo)
|
|
813
|
+
except ValueError as exc:
|
|
814
|
+
raise typer.BadParameter(str(exc), param_hint="--repo") from exc
|
|
815
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
816
|
+
out.write_text(patch_plan_to_markdown(plan), encoding="utf-8")
|
|
817
|
+
if json_out is not None:
|
|
818
|
+
_write_json(json_out, plan)
|
|
819
|
+
console.print(f"Wrote patch plan to {out}")
|
|
820
|
+
if json_out is not None:
|
|
821
|
+
console.print(f"Wrote patch plan JSON to {json_out}")
|
|
822
|
+
summary = plan["summary"]
|
|
823
|
+
console.print(
|
|
824
|
+
f"Patch plans: {summary['patch_count']}, "
|
|
825
|
+
f"unsupported findings: {summary['unsupported_finding_count']}, "
|
|
826
|
+
f"frameworks: {', '.join(summary['frameworks_detected']) or 'none'}"
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
@app.command("value-report")
|
|
831
|
+
def value_report(
|
|
832
|
+
path: Path = Path("runs/research_agent_baseline.json"),
|
|
833
|
+
out: Path = Path("runs/value_report.json"),
|
|
834
|
+
runs_per_month: int = typer.Option(1000, min=0),
|
|
835
|
+
engineer_hourly_rate_usd: float = typer.Option(150.0, min=0),
|
|
836
|
+
incident_cost_usd: float = typer.Option(500.0, min=0),
|
|
837
|
+
) -> None:
|
|
838
|
+
trace = _load_trace(path, param_hint="--path")
|
|
839
|
+
value = build_value_report(
|
|
840
|
+
trace,
|
|
841
|
+
runs_per_month=runs_per_month,
|
|
842
|
+
engineer_hourly_rate_usd=engineer_hourly_rate_usd,
|
|
843
|
+
incident_cost_usd=incident_cost_usd,
|
|
844
|
+
)
|
|
845
|
+
_write_json(out, value)
|
|
846
|
+
_print_value_summary(value)
|
|
847
|
+
console.print(f"Wrote value report to {out}")
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
@app.command("doctor")
|
|
851
|
+
def doctor_command(
|
|
852
|
+
check_api: bool = typer.Option(True, help="Also call the configured /health endpoint."),
|
|
853
|
+
json_out: Path | None = typer.Option(
|
|
854
|
+
None, help="Optional path for machine-readable doctor output."
|
|
855
|
+
),
|
|
856
|
+
) -> None:
|
|
857
|
+
result = run_doctor(check_api=check_api)
|
|
858
|
+
if json_out:
|
|
859
|
+
_write_json(json_out, result)
|
|
860
|
+
console.print(f"Wrote doctor output to {json_out}")
|
|
861
|
+
_print_doctor(result)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
@app.command("production-check")
|
|
865
|
+
def production_check_command(
|
|
866
|
+
check_api: bool = typer.Option(True, help="Call the configured /health and /readyz endpoints."),
|
|
867
|
+
check_store: bool = typer.Option(True, help="Initialize and query the configured store."),
|
|
868
|
+
allow_http: bool = typer.Option(
|
|
869
|
+
False, help="Allow non-HTTPS AGENTLOOP_API_URL for local staging."
|
|
870
|
+
),
|
|
871
|
+
json_out: Path | None = typer.Option(
|
|
872
|
+
None, help="Optional path for machine-readable check output."
|
|
873
|
+
),
|
|
874
|
+
) -> None:
|
|
875
|
+
result = run_production_check(
|
|
876
|
+
check_api=check_api, check_store=check_store, allow_http=allow_http
|
|
877
|
+
)
|
|
878
|
+
if json_out:
|
|
879
|
+
_write_json(json_out, result)
|
|
880
|
+
console.print(f"Wrote production check output to {json_out}")
|
|
881
|
+
_print_doctor(result)
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
@app.command("detect-integrations")
|
|
885
|
+
def detect_integrations_command(json_out: Path | None = typer.Option(None)) -> None:
|
|
886
|
+
"""Report which framework integrations are available in this environment.
|
|
887
|
+
|
|
888
|
+
Detection only: it checks whether each integration's SDK is importable and
|
|
889
|
+
does not instrument anything. Apply agentloop.integrations helpers from your
|
|
890
|
+
application startup to record traces.
|
|
891
|
+
"""
|
|
892
|
+
result = detect_integrations().to_dict()
|
|
893
|
+
if json_out:
|
|
894
|
+
_write_json(json_out, result)
|
|
895
|
+
console.print(f"Wrote integration detection output to {json_out}")
|
|
896
|
+
console.print_json(data=result)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
@app.command("auto-instrument", hidden=True, deprecated=True)
|
|
900
|
+
def auto_instrument_command(json_out: Path | None = typer.Option(None)) -> None:
|
|
901
|
+
"""Deprecated alias for `detect-integrations`; it only detects, never instruments."""
|
|
902
|
+
console.print(
|
|
903
|
+
"[yellow]`auto-instrument` is deprecated and never enabled instrumentation; "
|
|
904
|
+
"use `detect-integrations`.[/yellow]"
|
|
905
|
+
)
|
|
906
|
+
detect_integrations_command(json_out=json_out)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
@app.command("init-store")
|
|
910
|
+
def init_store() -> None:
|
|
911
|
+
db = get_store()
|
|
912
|
+
db.init()
|
|
913
|
+
console.print("Initialized AgentLoop store")
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
@app.command("create-api-key")
|
|
917
|
+
def create_api_key(project_id: str = "default", name: str = "default") -> None:
|
|
918
|
+
db = get_store()
|
|
919
|
+
key = db.create_api_key(project_id=project_id, name=name)
|
|
920
|
+
console.print("Created API key. Save it now; it will not be shown again.")
|
|
921
|
+
console.print(key["api_key"])
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
@app.command("store-trace")
|
|
925
|
+
def store_trace(
|
|
926
|
+
path: Path = Path("runs/research_agent_baseline.json"),
|
|
927
|
+
project_id: str = "default",
|
|
928
|
+
) -> None:
|
|
929
|
+
trace = _load_trace(path, param_hint="--path")
|
|
930
|
+
db = get_store()
|
|
931
|
+
db.save_trace(trace, project_id=project_id)
|
|
932
|
+
console.print(f"Stored trace {trace.run_id} under project {project_id}")
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
@app.command("list-stored-traces")
|
|
936
|
+
def list_stored_traces(
|
|
937
|
+
project_id: str | None = None,
|
|
938
|
+
limit: int | None = typer.Option(None, help="Page size; omit to list every stored trace."),
|
|
939
|
+
cursor: str | None = typer.Option(
|
|
940
|
+
None, help="Continuation cursor from a previous --limit call's printed next-cursor."
|
|
941
|
+
),
|
|
942
|
+
) -> None:
|
|
943
|
+
db = get_store()
|
|
944
|
+
if limit is None and cursor is None:
|
|
945
|
+
traces = db.list_traces(project_id=project_id)
|
|
946
|
+
next_cursor = None
|
|
947
|
+
else:
|
|
948
|
+
page = db.list_traces_page(
|
|
949
|
+
project_id=project_id, limit=limit or DEFAULT_PAGE_SIZE, cursor=cursor
|
|
950
|
+
)
|
|
951
|
+
traces = page["items"]
|
|
952
|
+
next_cursor = page["next_cursor"]
|
|
953
|
+
table = Table(title="Stored AgentLoop Traces")
|
|
954
|
+
table.add_column("Project")
|
|
955
|
+
table.add_column("Run ID")
|
|
956
|
+
table.add_column("Name")
|
|
957
|
+
table.add_column("Runtime ms")
|
|
958
|
+
table.add_column("Cost")
|
|
959
|
+
for item in traces:
|
|
960
|
+
table.add_row(
|
|
961
|
+
str(item.get("project_id", "")),
|
|
962
|
+
str(item.get("run_id", "")),
|
|
963
|
+
str(item.get("name", "")),
|
|
964
|
+
"unavailable"
|
|
965
|
+
if item.get("total_runtime_ms") is None
|
|
966
|
+
else f"{float(item.get('total_runtime_ms', 0)):.2f}",
|
|
967
|
+
format_cost_usd(item.get("estimated_cost_usd"), item.get("cost_status", "complete")),
|
|
968
|
+
)
|
|
969
|
+
console.print(table)
|
|
970
|
+
if next_cursor:
|
|
971
|
+
console.print(f"More results available. Next cursor: {next_cursor}")
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
@app.command("list-findings")
|
|
975
|
+
def list_findings(
|
|
976
|
+
project_id: str | None = None,
|
|
977
|
+
status: str | None = None,
|
|
978
|
+
limit: int | None = typer.Option(None, help="Page size; omit to list every matching finding."),
|
|
979
|
+
cursor: str | None = typer.Option(
|
|
980
|
+
None, help="Continuation cursor from a previous --limit call's printed next-cursor."
|
|
981
|
+
),
|
|
982
|
+
) -> None:
|
|
983
|
+
db = get_store()
|
|
984
|
+
if limit is None and cursor is None:
|
|
985
|
+
findings = db.list_findings(project_id=project_id, status=status)
|
|
986
|
+
next_cursor = None
|
|
987
|
+
else:
|
|
988
|
+
page = db.list_findings_page(
|
|
989
|
+
project_id=project_id, status=status, limit=limit or DEFAULT_PAGE_SIZE, cursor=cursor
|
|
990
|
+
)
|
|
991
|
+
findings = page["items"]
|
|
992
|
+
next_cursor = page["next_cursor"]
|
|
993
|
+
table = Table(title="AgentLoop Findings")
|
|
994
|
+
table.add_column("Severity")
|
|
995
|
+
table.add_column("Type")
|
|
996
|
+
table.add_column("Title")
|
|
997
|
+
table.add_column("Run")
|
|
998
|
+
table.add_column("Status")
|
|
999
|
+
table.add_column("Patchable")
|
|
1000
|
+
table.add_column("Savings")
|
|
1001
|
+
for finding in findings:
|
|
1002
|
+
table.add_row(
|
|
1003
|
+
str(finding["severity"]),
|
|
1004
|
+
str(finding["type"]),
|
|
1005
|
+
str(finding["title"]),
|
|
1006
|
+
str(finding["run_id"]),
|
|
1007
|
+
str(finding["status"]),
|
|
1008
|
+
"yes" if finding["patchable"] else "no",
|
|
1009
|
+
f"{format_duration_ms(finding['estimated_latency_savings_ms'])} / "
|
|
1010
|
+
f"{format_cost_usd(finding.get('estimated_cost_savings_usd'))}",
|
|
1011
|
+
)
|
|
1012
|
+
console.print(table)
|
|
1013
|
+
if next_cursor:
|
|
1014
|
+
console.print(f"More results available. Next cursor: {next_cursor}")
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
@app.command("update-finding-status")
|
|
1018
|
+
def update_finding_status_command(
|
|
1019
|
+
run_id: str,
|
|
1020
|
+
finding_id: str,
|
|
1021
|
+
status: str,
|
|
1022
|
+
project_id: str = "default",
|
|
1023
|
+
) -> None:
|
|
1024
|
+
"""Transition a stored finding's lifecycle status (detected/accepted/resolved/dismissed)."""
|
|
1025
|
+
db = get_store()
|
|
1026
|
+
try:
|
|
1027
|
+
updated = db.update_finding_status(project_id, run_id, finding_id, status)
|
|
1028
|
+
except FindingNotFoundError as exc:
|
|
1029
|
+
raise typer.BadParameter(str(exc), param_hint="finding_id") from exc
|
|
1030
|
+
except (FindingTransitionError, ValueError) as exc:
|
|
1031
|
+
raise typer.BadParameter(str(exc), param_hint="status") from exc
|
|
1032
|
+
console.print(f"Finding {finding_id} is now [bold]{updated['status']}[/bold]")
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
@app.command("remote-update-finding-status")
|
|
1036
|
+
def remote_update_finding_status(
|
|
1037
|
+
run_id: str,
|
|
1038
|
+
finding_id: str,
|
|
1039
|
+
status: str,
|
|
1040
|
+
api_url: str = typer.Option("http://127.0.0.1:8000", help="AgentLoop API base URL."),
|
|
1041
|
+
api_key: str | None = typer.Option(
|
|
1042
|
+
None, help="Optional API key. Defaults to AGENTLOOP_API_KEY."
|
|
1043
|
+
),
|
|
1044
|
+
) -> None:
|
|
1045
|
+
client = _remote_client(api_url, api_key)
|
|
1046
|
+
updated = client.update_finding_status(run_id, finding_id, status)
|
|
1047
|
+
console.print_json(data=updated)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
@app.command("optimization-queue")
|
|
1051
|
+
def optimization_queue(
|
|
1052
|
+
project_id: str | None = None,
|
|
1053
|
+
json_out: Path | None = typer.Option(None),
|
|
1054
|
+
sort_by: RankingSort = typer.Option(
|
|
1055
|
+
RankingSort.PRIORITY, help="Investigation ranking dimension."
|
|
1056
|
+
),
|
|
1057
|
+
) -> None:
|
|
1058
|
+
db = get_store()
|
|
1059
|
+
queue = sort_ranked(db.optimization_queue(project_id=project_id), sort_by.value)
|
|
1060
|
+
payload = {"project_id": project_id, "sort_by": sort_by.value, "queue": queue}
|
|
1061
|
+
if json_out is not None:
|
|
1062
|
+
_write_json(json_out, payload)
|
|
1063
|
+
console.print(f"Wrote optimization queue JSON to {json_out}")
|
|
1064
|
+
table = Table(title="AgentLoop Optimization Queue")
|
|
1065
|
+
table.add_column("Priority / readiness")
|
|
1066
|
+
table.add_column("Severity")
|
|
1067
|
+
table.add_column("Type")
|
|
1068
|
+
table.add_column("Title")
|
|
1069
|
+
table.add_column("Runs")
|
|
1070
|
+
table.add_column("Patchable")
|
|
1071
|
+
table.add_column("Savings")
|
|
1072
|
+
for item in queue:
|
|
1073
|
+
table.add_row(
|
|
1074
|
+
f"{item['ranking']['priority_rank']} / {item['ranking']['status']}",
|
|
1075
|
+
str(item["severity"]),
|
|
1076
|
+
str(item["type"]),
|
|
1077
|
+
str(item["title"]),
|
|
1078
|
+
str(item["run_count"]),
|
|
1079
|
+
str(item["patchable_count"]),
|
|
1080
|
+
f"{format_duration_ms(item['estimated_latency_savings_ms'])} / "
|
|
1081
|
+
f"{format_cost_usd(item.get('estimated_cost_savings_usd'))}",
|
|
1082
|
+
)
|
|
1083
|
+
console.print(table)
|
|
1084
|
+
|
|
1085
|
+
for item in queue:
|
|
1086
|
+
console.print(
|
|
1087
|
+
f"{item['title']}: "
|
|
1088
|
+
+ (", ".join(item["ranking"]["reasons"]) or "Required ranking inputs are complete."),
|
|
1089
|
+
markup=False,
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
@app.command("github-issue-drafts")
|
|
1094
|
+
def github_issue_drafts(
|
|
1095
|
+
project_id: str | None = None,
|
|
1096
|
+
out: Path = Path("runs/agentloop_issue_drafts.md"),
|
|
1097
|
+
json_out: Path | None = typer.Option(None),
|
|
1098
|
+
limit: int = typer.Option(5, min=1, max=20),
|
|
1099
|
+
) -> None:
|
|
1100
|
+
db = get_store()
|
|
1101
|
+
drafts = build_issue_drafts(db.optimization_queue(project_id=project_id), limit=limit)
|
|
1102
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
1103
|
+
out.write_text(issue_drafts_to_markdown(drafts), encoding="utf-8")
|
|
1104
|
+
if json_out is not None:
|
|
1105
|
+
_write_json(json_out, {"project_id": project_id, "issue_drafts": drafts})
|
|
1106
|
+
console.print(f"Wrote GitHub issue draft JSON to {json_out}")
|
|
1107
|
+
console.print(f"Wrote GitHub issue drafts to {out}")
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
@app.command("usage-summary")
|
|
1111
|
+
def usage_summary(project_id: str | None = None) -> None:
|
|
1112
|
+
db = get_store()
|
|
1113
|
+
summary = db.usage_summary(project_id=project_id)
|
|
1114
|
+
table = Table(title="AgentLoop Usage Summary")
|
|
1115
|
+
table.add_column("Metric")
|
|
1116
|
+
table.add_column("Value")
|
|
1117
|
+
for key, value in summary.items():
|
|
1118
|
+
rendered = (
|
|
1119
|
+
format_cost_usd(value, summary.get("cost_status", "complete"))
|
|
1120
|
+
if key == "estimated_cost_usd"
|
|
1121
|
+
else str(value)
|
|
1122
|
+
)
|
|
1123
|
+
table.add_row(key, rendered)
|
|
1124
|
+
console.print(table)
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
@app.command()
|
|
1128
|
+
def upload(
|
|
1129
|
+
path: Path = Path("runs/research_agent_baseline.json"),
|
|
1130
|
+
api_url: str = typer.Option("http://127.0.0.1:8000", help="AgentLoop API base URL."),
|
|
1131
|
+
api_key: str | None = typer.Option(
|
|
1132
|
+
None, help="Optional API key. Defaults to AGENTLOOP_API_KEY."
|
|
1133
|
+
),
|
|
1134
|
+
) -> None:
|
|
1135
|
+
trace = _load_trace(path, param_hint="--path")
|
|
1136
|
+
client = _remote_client(api_url, api_key)
|
|
1137
|
+
response = client.upload_trace(trace)
|
|
1138
|
+
console.print(f"Uploaded trace {response['run_id']} to {api_url}")
|
|
1139
|
+
console.print(f"Project: {response.get('project_id', 'default')}")
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
@app.command("remote-optimize")
|
|
1143
|
+
def remote_optimize(
|
|
1144
|
+
run_id: str,
|
|
1145
|
+
api_url: str = typer.Option("http://127.0.0.1:8000", help="AgentLoop API base URL."),
|
|
1146
|
+
api_key: str | None = typer.Option(
|
|
1147
|
+
None, help="Optional API key. Defaults to AGENTLOOP_API_KEY."
|
|
1148
|
+
),
|
|
1149
|
+
out: Path = Path("runs/remote_optimization_plan.json"),
|
|
1150
|
+
) -> None:
|
|
1151
|
+
client = _remote_client(api_url, api_key)
|
|
1152
|
+
plan = client.get_optimization_plan(run_id)
|
|
1153
|
+
export_optimization_json(plan, out)
|
|
1154
|
+
console.print(f"Wrote remote optimization plan to {out}")
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
@app.command("remote-diagnose", help="Recompute and persist findings for a stored remote trace.")
|
|
1158
|
+
def remote_diagnose(
|
|
1159
|
+
run_id: str,
|
|
1160
|
+
api_url: str = typer.Option("http://127.0.0.1:8000", help="AgentLoop API base URL."),
|
|
1161
|
+
api_key: str | None = typer.Option(
|
|
1162
|
+
None, help="Optional API key. Defaults to AGENTLOOP_API_KEY."
|
|
1163
|
+
),
|
|
1164
|
+
out: Path = Path("runs/remote_diagnosis.json"),
|
|
1165
|
+
) -> None:
|
|
1166
|
+
client = _remote_client(api_url, api_key)
|
|
1167
|
+
diagnosis = client.save_diagnosis(run_id)
|
|
1168
|
+
_write_json(out, diagnosis)
|
|
1169
|
+
console.print(f"Wrote remote diagnosis to {out}")
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
@app.command("remote-findings")
|
|
1173
|
+
def remote_findings(
|
|
1174
|
+
api_url: str = typer.Option("http://127.0.0.1:8000", help="AgentLoop API base URL."),
|
|
1175
|
+
api_key: str | None = typer.Option(
|
|
1176
|
+
None, help="Optional API key. Defaults to AGENTLOOP_API_KEY."
|
|
1177
|
+
),
|
|
1178
|
+
project_id: str | None = typer.Option(None),
|
|
1179
|
+
status: str | None = typer.Option(None),
|
|
1180
|
+
out: Path = Path("runs/remote_findings.json"),
|
|
1181
|
+
) -> None:
|
|
1182
|
+
client = _remote_client(api_url, api_key)
|
|
1183
|
+
findings = client.list_findings(project_id=project_id, status=status)
|
|
1184
|
+
_write_json(out, findings)
|
|
1185
|
+
console.print(f"Wrote remote findings to {out}")
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
@app.command("remote-optimization-queue")
|
|
1189
|
+
def remote_optimization_queue(
|
|
1190
|
+
api_url: str = typer.Option("http://127.0.0.1:8000", help="AgentLoop API base URL."),
|
|
1191
|
+
api_key: str | None = typer.Option(
|
|
1192
|
+
None, help="Optional API key. Defaults to AGENTLOOP_API_KEY."
|
|
1193
|
+
),
|
|
1194
|
+
project_id: str | None = typer.Option(None),
|
|
1195
|
+
out: Path = Path("runs/remote_optimization_queue.json"),
|
|
1196
|
+
) -> None:
|
|
1197
|
+
client = _remote_client(api_url, api_key)
|
|
1198
|
+
queue = client.optimization_queue(project_id=project_id)
|
|
1199
|
+
_write_json(out, queue)
|
|
1200
|
+
console.print(f"Wrote remote optimization queue to {out}")
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
@app.command("remote-github-issue-drafts")
|
|
1204
|
+
def remote_github_issue_drafts(
|
|
1205
|
+
api_url: str = typer.Option("http://127.0.0.1:8000", help="AgentLoop API base URL."),
|
|
1206
|
+
api_key: str | None = typer.Option(
|
|
1207
|
+
None, help="Optional API key. Defaults to AGENTLOOP_API_KEY."
|
|
1208
|
+
),
|
|
1209
|
+
project_id: str | None = typer.Option(None),
|
|
1210
|
+
limit: int = typer.Option(5, min=1, max=20),
|
|
1211
|
+
out: Path = Path("runs/remote_agentloop_issue_drafts.json"),
|
|
1212
|
+
) -> None:
|
|
1213
|
+
client = _remote_client(api_url, api_key)
|
|
1214
|
+
drafts = client.github_issue_drafts(project_id=project_id, limit=limit)
|
|
1215
|
+
_write_json(out, drafts)
|
|
1216
|
+
console.print(f"Wrote remote GitHub issue drafts to {out}")
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
@app.command("remote-value-report")
|
|
1220
|
+
def remote_value_report(
|
|
1221
|
+
run_id: str,
|
|
1222
|
+
api_url: str = typer.Option("http://127.0.0.1:8000", help="AgentLoop API base URL."),
|
|
1223
|
+
api_key: str | None = typer.Option(
|
|
1224
|
+
None, help="Optional API key. Defaults to AGENTLOOP_API_KEY."
|
|
1225
|
+
),
|
|
1226
|
+
out: Path = Path("runs/remote_value_report.json"),
|
|
1227
|
+
runs_per_month: int = typer.Option(1000, min=0),
|
|
1228
|
+
engineer_hourly_rate_usd: float = typer.Option(150.0, min=0),
|
|
1229
|
+
incident_cost_usd: float = typer.Option(500.0, min=0),
|
|
1230
|
+
) -> None:
|
|
1231
|
+
client = _remote_client(api_url, api_key)
|
|
1232
|
+
value = client.get_value_report(
|
|
1233
|
+
run_id,
|
|
1234
|
+
runs_per_month=runs_per_month,
|
|
1235
|
+
engineer_hourly_rate_usd=engineer_hourly_rate_usd,
|
|
1236
|
+
incident_cost_usd=incident_cost_usd,
|
|
1237
|
+
)
|
|
1238
|
+
_write_json(out, value)
|
|
1239
|
+
_print_value_summary(value)
|
|
1240
|
+
console.print(f"Wrote remote value report to {out}")
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
@app.command("remote-usage")
|
|
1244
|
+
def remote_usage(
|
|
1245
|
+
api_url: str = typer.Option("http://127.0.0.1:8000", help="AgentLoop API base URL."),
|
|
1246
|
+
api_key: str | None = typer.Option(
|
|
1247
|
+
None, help="Optional API key. Defaults to AGENTLOOP_API_KEY."
|
|
1248
|
+
),
|
|
1249
|
+
) -> None:
|
|
1250
|
+
client = _remote_client(api_url, api_key)
|
|
1251
|
+
console.print_json(data=client.usage_summary())
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
@app.command("remote-create-api-key")
|
|
1255
|
+
def remote_create_api_key(
|
|
1256
|
+
project_id: str = "default",
|
|
1257
|
+
name: str = "default",
|
|
1258
|
+
api_url: str = typer.Option("http://127.0.0.1:8000", help="AgentLoop API base URL."),
|
|
1259
|
+
admin_api_key: str | None = typer.Option(
|
|
1260
|
+
None, help="Admin API key. Defaults to AGENTLOOP_ADMIN_API_KEY."
|
|
1261
|
+
),
|
|
1262
|
+
) -> None:
|
|
1263
|
+
client = _remote_admin_client(api_url, admin_api_key)
|
|
1264
|
+
key = client.create_api_key(project_id=project_id, name=name)
|
|
1265
|
+
console.print("Created remote API key. Save it now; it will not be shown again.")
|
|
1266
|
+
console.print(key["api_key"])
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
@app.command()
|
|
1270
|
+
def demo(
|
|
1271
|
+
kind: str = typer.Option("baseline", help="baseline, optimized, langgraph, or proof"),
|
|
1272
|
+
) -> None:
|
|
1273
|
+
if kind == "baseline":
|
|
1274
|
+
path = run_baseline()
|
|
1275
|
+
elif kind == "optimized":
|
|
1276
|
+
path = run_optimized()
|
|
1277
|
+
elif kind == "langgraph":
|
|
1278
|
+
path = run_langgraph_style()
|
|
1279
|
+
elif kind == "proof":
|
|
1280
|
+
baseline, candidate = run_proof_pair()
|
|
1281
|
+
console.print(f"Wrote synthetic demo trace to {baseline}")
|
|
1282
|
+
console.print(f"Wrote synthetic demo trace to {candidate}")
|
|
1283
|
+
return
|
|
1284
|
+
else:
|
|
1285
|
+
raise typer.BadParameter("kind must be 'baseline', 'optimized', 'langgraph', or 'proof'")
|
|
1286
|
+
console.print(f"Wrote synthetic demo trace to {path}")
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
@app.command("demo-all")
|
|
1290
|
+
def demo_all() -> None:
|
|
1291
|
+
base = run_baseline()
|
|
1292
|
+
opt = run_optimized()
|
|
1293
|
+
lg = run_langgraph_style()
|
|
1294
|
+
proof_base, proof_candidate = run_proof_pair()
|
|
1295
|
+
console.print(f"Wrote synthetic demo trace to {base}")
|
|
1296
|
+
console.print(f"Wrote synthetic demo trace to {opt}")
|
|
1297
|
+
console.print(f"Wrote synthetic demo trace to {lg}")
|
|
1298
|
+
console.print(f"Wrote synthetic demo trace to {proof_base}")
|
|
1299
|
+
console.print(f"Wrote synthetic demo trace to {proof_candidate}")
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
@app.command()
|
|
1303
|
+
def server(host: str = "127.0.0.1", port: int = 8000, reload: bool = False) -> None:
|
|
1304
|
+
try:
|
|
1305
|
+
import uvicorn
|
|
1306
|
+
except ImportError as exc:
|
|
1307
|
+
raise typer.BadParameter(
|
|
1308
|
+
"Install server dependencies with: uv sync --locked --extra server"
|
|
1309
|
+
) from exc
|
|
1310
|
+
uvicorn.run("agentloop.server:app", host=host, port=port, reload=reload)
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
if __name__ == "__main__":
|
|
1314
|
+
app()
|