agentloop-profiler 0.4.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.

Potentially problematic release.


This version of agentloop-profiler might be problematic. Click here for more details.

Files changed (50) hide show
  1. agentloop/__init__.py +68 -0
  2. agentloop/audit.py +34 -0
  3. agentloop/autoinstrument.py +102 -0
  4. agentloop/ci.py +135 -0
  5. agentloop/cli.py +876 -0
  6. agentloop/client.py +170 -0
  7. agentloop/config.py +50 -0
  8. agentloop/costs.py +15 -0
  9. agentloop/decorators.py +160 -0
  10. agentloop/demo.py +283 -0
  11. agentloop/doctor.py +344 -0
  12. agentloop/events.py +49 -0
  13. agentloop/exporters.py +60 -0
  14. agentloop/findings.py +215 -0
  15. agentloop/graph.py +312 -0
  16. agentloop/integrations/__init__.py +1 -0
  17. agentloop/integrations/crewai.py +144 -0
  18. agentloop/integrations/langgraph.py +140 -0
  19. agentloop/integrations/openai.py +369 -0
  20. agentloop/integrations/openai_agents.py +212 -0
  21. agentloop/integrations/sdk.py +82 -0
  22. agentloop/integrations/vercel_ai.py +83 -0
  23. agentloop/issues.py +88 -0
  24. agentloop/markdown.py +102 -0
  25. agentloop/metrics.py +123 -0
  26. agentloop/optimizer.py +323 -0
  27. agentloop/otel.py +302 -0
  28. agentloop/otel_ids.py +81 -0
  29. agentloop/patches.py +583 -0
  30. agentloop/plan_export.py +72 -0
  31. agentloop/quality.py +222 -0
  32. agentloop/replay.py +424 -0
  33. agentloop/runtime.py +244 -0
  34. agentloop/savings.py +135 -0
  35. agentloop/server.py +292 -0
  36. agentloop/store.py +840 -0
  37. agentloop/timing.py +124 -0
  38. agentloop/tracer.py +356 -0
  39. agentloop/value.py +243 -0
  40. agentloop/version.py +3 -0
  41. agentloop_profiler-0.4.0.dist-info/METADATA +598 -0
  42. agentloop_profiler-0.4.0.dist-info/RECORD +50 -0
  43. agentloop_profiler-0.4.0.dist-info/WHEEL +5 -0
  44. agentloop_profiler-0.4.0.dist-info/entry_points.txt +2 -0
  45. agentloop_profiler-0.4.0.dist-info/licenses/LICENSE +201 -0
  46. agentloop_profiler-0.4.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +54 -0
  47. agentloop_profiler-0.4.0.dist-info/top_level.txt +2 -0
  48. dashboard/__init__.py +1 -0
  49. dashboard/app.py +698 -0
  50. dashboard/value_view.py +101 -0
agentloop/client.py ADDED
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import urllib.error
6
+ import urllib.parse
7
+ import urllib.request
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from agentloop.tracer import AgentTrace
13
+
14
+
15
+ class AgentLoopClientError(RuntimeError):
16
+ pass
17
+
18
+
19
+ @dataclass
20
+ class AgentLoopClient:
21
+ """Tiny dependency-free client for sending traces to an AgentLoop API server."""
22
+
23
+ base_url: str = "http://127.0.0.1:8000"
24
+ api_key: str | None = None
25
+ admin_api_key: str | None = None
26
+ timeout_s: float = 10.0
27
+
28
+ @classmethod
29
+ def from_env(cls) -> "AgentLoopClient":
30
+ return cls(
31
+ base_url=os.getenv("AGENTLOOP_API_URL", "http://127.0.0.1:8000"),
32
+ api_key=os.getenv("AGENTLOOP_API_KEY"),
33
+ admin_api_key=os.getenv("AGENTLOOP_ADMIN_API_KEY"),
34
+ timeout_s=float(os.getenv("AGENTLOOP_TIMEOUT_S", "10")),
35
+ )
36
+
37
+ def health(self) -> dict[str, Any]:
38
+ return self._request("GET", "/health")
39
+
40
+ def create_api_key(self, project_id: str = "default", name: str = "default") -> dict[str, Any]:
41
+ return self._request("POST", "/api-keys", {"project_id": project_id, "name": name})
42
+
43
+ def upload_trace(self, trace: AgentTrace | str | Path | dict[str, Any]) -> dict[str, Any]:
44
+ if isinstance(trace, AgentTrace):
45
+ payload = trace.to_dict()
46
+ elif isinstance(trace, dict):
47
+ payload = trace
48
+ else:
49
+ payload = json.loads(Path(trace).read_text(encoding="utf-8"))
50
+ return self._request("POST", "/traces", payload)
51
+
52
+ def list_traces(self, project_id: str | None = None) -> dict[str, Any]:
53
+ route = "/traces"
54
+ if project_id:
55
+ route += "?" + urllib.parse.urlencode({"project_id": project_id})
56
+ return self._request("GET", route)
57
+
58
+ def usage_summary(self, project_id: str | None = None) -> dict[str, Any]:
59
+ route = "/usage"
60
+ if project_id:
61
+ route += "?" + urllib.parse.urlencode({"project_id": project_id})
62
+ return self._request("GET", route)
63
+
64
+ def get_report(self, run_id: str) -> dict[str, Any]:
65
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
66
+ return self._request("GET", f"/traces/{encoded_run_id}/report")
67
+
68
+ def get_optimization_plan(self, run_id: str) -> dict[str, Any]:
69
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
70
+ return self._request("GET", f"/traces/{encoded_run_id}/optimize")
71
+
72
+ def get_diagnosis(self, run_id: str) -> dict[str, Any]:
73
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
74
+ return self._request("GET", f"/traces/{encoded_run_id}/diagnose")
75
+
76
+ def list_findings(
77
+ self, project_id: str | None = None, status: str | None = None
78
+ ) -> dict[str, Any]:
79
+ params: dict[str, Any] = {}
80
+ if project_id:
81
+ params["project_id"] = project_id
82
+ if status:
83
+ params["status"] = status
84
+ route = "/findings"
85
+ if params:
86
+ route += "?" + urllib.parse.urlencode(params)
87
+ return self._request("GET", route)
88
+
89
+ def optimization_queue(self, project_id: str | None = None) -> dict[str, Any]:
90
+ route = "/optimization-queue"
91
+ if project_id:
92
+ route += "?" + urllib.parse.urlencode({"project_id": project_id})
93
+ return self._request("GET", route)
94
+
95
+ def github_issue_drafts(self, project_id: str | None = None, limit: int = 5) -> dict[str, Any]:
96
+ params: dict[str, Any] = {"limit": limit}
97
+ if project_id:
98
+ params["project_id"] = project_id
99
+ return self._request(
100
+ "GET", "/optimization-queue/github-issues?" + urllib.parse.urlencode(params)
101
+ )
102
+
103
+ def build_quality_report(
104
+ self,
105
+ fixtures: list[dict[str, Any]],
106
+ *,
107
+ baseline_run_id: str | None = None,
108
+ candidate_run_id: str | None = None,
109
+ min_score: float | None = None,
110
+ ) -> dict[str, Any]:
111
+ return self._request(
112
+ "POST",
113
+ "/quality-report",
114
+ {
115
+ "fixtures": fixtures,
116
+ "baseline_run_id": baseline_run_id,
117
+ "candidate_run_id": candidate_run_id,
118
+ "min_score": min_score,
119
+ },
120
+ )
121
+
122
+ def get_value_report(
123
+ self,
124
+ run_id: str,
125
+ *,
126
+ runs_per_month: int | None = None,
127
+ engineer_hourly_rate_usd: float | None = None,
128
+ incident_cost_usd: float | None = None,
129
+ ) -> dict[str, Any]:
130
+ params: dict[str, Any] = {}
131
+ if runs_per_month is not None:
132
+ params["runs_per_month"] = runs_per_month
133
+ if engineer_hourly_rate_usd is not None:
134
+ params["engineer_hourly_rate_usd"] = engineer_hourly_rate_usd
135
+ if incident_cost_usd is not None:
136
+ params["incident_cost_usd"] = incident_cost_usd
137
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
138
+ route = f"/traces/{encoded_run_id}/value"
139
+ if params:
140
+ route += "?" + urllib.parse.urlencode(params)
141
+ return self._request("GET", route)
142
+
143
+ def _request(
144
+ self, method: str, path: str, payload: dict[str, Any] | None = None
145
+ ) -> dict[str, Any]:
146
+ parsed_base_url = urllib.parse.urlsplit(self.base_url)
147
+ if parsed_base_url.scheme not in {"http", "https"} or not parsed_base_url.netloc:
148
+ raise AgentLoopClientError("AgentLoop API URL must use http:// or https://")
149
+ url = self.base_url.rstrip("/") + path
150
+ body = None if payload is None else json.dumps(payload).encode("utf-8")
151
+ headers = {"Accept": "application/json"}
152
+ if payload is not None:
153
+ headers["Content-Type"] = "application/json"
154
+ if self.api_key:
155
+ headers["X-AgentLoop-Key"] = self.api_key
156
+ if self.admin_api_key:
157
+ headers["X-AgentLoop-Admin-Key"] = self.admin_api_key
158
+ req = urllib.request.Request(url=url, data=body, method=method, headers=headers)
159
+ try:
160
+ with urllib.request.urlopen( # nosec B310 - base URL scheme is validated above.
161
+ req, timeout=self.timeout_s
162
+ ) as response:
163
+ return json.loads(response.read().decode("utf-8"))
164
+ except urllib.error.HTTPError as exc:
165
+ detail = exc.read().decode("utf-8", errors="ignore")
166
+ raise AgentLoopClientError(f"AgentLoop API returned {exc.code}: {detail}") from exc
167
+ except urllib.error.URLError as exc:
168
+ raise AgentLoopClientError(
169
+ f"Could not reach AgentLoop API at {url}: {exc.reason}"
170
+ ) from exc
agentloop/config.py ADDED
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ _POSTGRES_URL_ENV_VARS = ("AGENTLOOP_DATABASE_URL", "DATABASE_URL")
6
+ _POSTGRES_LIBPQ_SOURCE_ENV_VARS = ("PGHOST", "PGHOSTADDR", "PGSERVICE")
7
+
8
+
9
+ def get_api_key() -> str | None:
10
+ return os.getenv("AGENTLOOP_API_KEY")
11
+
12
+
13
+ def get_admin_api_key() -> str | None:
14
+ return os.getenv("AGENTLOOP_ADMIN_API_KEY")
15
+
16
+
17
+ def require_api_key() -> bool:
18
+ return os.getenv("AGENTLOOP_REQUIRE_API_KEY", "false").lower() in {"1", "true", "yes", "on"}
19
+
20
+
21
+ def get_api_url() -> str:
22
+ return os.getenv("AGENTLOOP_API_URL", "http://127.0.0.1:8000")
23
+
24
+
25
+ def get_cors_origins() -> list[str]:
26
+ value = os.getenv("AGENTLOOP_CORS_ORIGINS", "")
27
+ return [origin.strip() for origin in value.split(",") if origin.strip()]
28
+
29
+
30
+ def get_postgres_dsn() -> str | None:
31
+ """Return the explicit Postgres DSN, preserving AgentLoop override precedence."""
32
+ for name in _POSTGRES_URL_ENV_VARS:
33
+ value = os.getenv(name)
34
+ if value:
35
+ return value
36
+ return None
37
+
38
+
39
+ def get_postgres_password_file() -> str | None:
40
+ """Return the optional file containing a libpq password."""
41
+ return os.getenv("AGENTLOOP_POSTGRES_PASSWORD_FILE") or None
42
+
43
+
44
+ def postgres_connection_source() -> str | None:
45
+ """Describe the configured Postgres connection source without exposing secrets."""
46
+ if get_postgres_dsn():
47
+ return "database URL"
48
+ if any(os.getenv(name) for name in _POSTGRES_LIBPQ_SOURCE_ENV_VARS):
49
+ return "libpq environment"
50
+ return None
agentloop/costs.py ADDED
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ MODEL_PRICES = {
4
+ "gpt-4.1": {"input": 2.00, "output": 8.00},
5
+ "gpt-4.1-mini": {"input": 0.40, "output": 1.60},
6
+ "gpt-4o-mini": {"input": 0.15, "output": 0.60},
7
+ "default": {"input": 1.00, "output": 3.00},
8
+ }
9
+
10
+
11
+ def estimate_cost_usd(model: str | None, input_tokens: int, output_tokens: int) -> float:
12
+ price = MODEL_PRICES.get(model or "default", MODEL_PRICES["default"])
13
+ input_cost = (input_tokens / 1000000) * price["input"]
14
+ output_cost = (output_tokens / 1000000) * price["output"]
15
+ return round(input_cost + output_cost, 6)
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from functools import wraps
5
+ from typing import Any, Callable, TypeVar, overload
6
+
7
+ from agentloop.tracer import current_trace, trace_agent, trace_model_call, trace_tool_call
8
+
9
+ F = TypeVar("F", bound=Callable[..., Any])
10
+
11
+
12
+ def _call_metadata(args: tuple[Any, ...], kwargs: dict[str, Any]) -> dict[str, Any]:
13
+ metadata: dict[str, Any] = {"integration": "agentloop.decorator"}
14
+ if args:
15
+ metadata["arg_count"] = len(args)
16
+ if kwargs:
17
+ metadata["kwarg_keys"] = sorted(str(key) for key in kwargs.keys())
18
+ return metadata
19
+
20
+
21
+ @overload
22
+ def traceable(fn: F) -> F: ...
23
+
24
+
25
+ @overload
26
+ def traceable(
27
+ fn: None = None,
28
+ *,
29
+ name: str | None = None,
30
+ kind: str = "tool",
31
+ model: str | None = None,
32
+ root: bool = False,
33
+ agent_name: str | None = None,
34
+ ) -> Callable[[F], F]: ...
35
+
36
+
37
+ def traceable(
38
+ fn: F | None = None,
39
+ *,
40
+ name: str | None = None,
41
+ kind: str = "tool",
42
+ model: str | None = None,
43
+ root: bool = False,
44
+ agent_name: str | None = None,
45
+ ) -> F | Callable[[F], F]:
46
+ """Trace a normal Python function without changing its implementation.
47
+
48
+ Examples:
49
+
50
+ ```python
51
+ import agentloop
52
+
53
+
54
+ @agentloop.traceable(root=True, agent_name="research_agent")
55
+ def run_agent(question: str):
56
+ return plan(question)
57
+
58
+
59
+ @agentloop.traceable(kind="model", model="gpt-4.1-mini")
60
+ def plan(question: str):
61
+ return client.responses.create(...)
62
+
63
+
64
+ @agentloop.traceable(kind="tool")
65
+ def search_web(query: str):
66
+ return search(query)
67
+ ```
68
+
69
+ `root=True` starts an AgentLoop run when no active run exists. Nested calls are
70
+ recorded as spans inside that run. This gives teams a low-friction migration
71
+ path when they do not use LangGraph, CrewAI, or another supported framework.
72
+ """
73
+
74
+ normalized_kind = kind.lower().replace("-", "_")
75
+ if normalized_kind not in {"tool", "model"}:
76
+ raise ValueError("kind must be 'tool' or 'model'")
77
+
78
+ def decorator(func: F) -> F:
79
+ span_name = name or getattr(
80
+ func, "__name__", getattr(func, "__qualname__", "agentloop.call")
81
+ )
82
+ run_name = agent_name or span_name
83
+
84
+ if inspect.iscoroutinefunction(func):
85
+
86
+ @wraps(func)
87
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
88
+ if root and current_trace() is None:
89
+ with trace_agent(run_name, metadata={"entrypoint": span_name}):
90
+ return await _run_async_span(
91
+ func, span_name, normalized_kind, model, args, kwargs
92
+ )
93
+ return await _run_async_span(func, span_name, normalized_kind, model, args, kwargs)
94
+
95
+ return async_wrapper # type: ignore[return-value]
96
+
97
+ @wraps(func)
98
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
99
+ if root and current_trace() is None:
100
+ with trace_agent(run_name, metadata={"entrypoint": span_name}):
101
+ return _run_sync_span(func, span_name, normalized_kind, model, args, kwargs)
102
+ return _run_sync_span(func, span_name, normalized_kind, model, args, kwargs)
103
+
104
+ return wrapper # type: ignore[return-value]
105
+
106
+ if fn is not None:
107
+ return decorator(fn)
108
+ return decorator
109
+
110
+
111
+ async def _run_async_span(
112
+ func: Callable[..., Any],
113
+ span_name: str,
114
+ kind: str,
115
+ model: str | None,
116
+ args: tuple[Any, ...],
117
+ kwargs: dict[str, Any],
118
+ ) -> Any:
119
+ metadata = _call_metadata(args, kwargs)
120
+ if kind == "model":
121
+ with trace_model_call(span_name, model=model, metadata=metadata):
122
+ return await func(*args, **kwargs)
123
+ with trace_tool_call(span_name, metadata=metadata):
124
+ return await func(*args, **kwargs)
125
+
126
+
127
+ def _run_sync_span(
128
+ func: Callable[..., Any],
129
+ span_name: str,
130
+ kind: str,
131
+ model: str | None,
132
+ args: tuple[Any, ...],
133
+ kwargs: dict[str, Any],
134
+ ) -> Any:
135
+ metadata = _call_metadata(args, kwargs)
136
+ if kind == "model":
137
+ with trace_model_call(span_name, model=model, metadata=metadata):
138
+ return func(*args, **kwargs)
139
+ with trace_tool_call(span_name, metadata=metadata):
140
+ return func(*args, **kwargs)
141
+
142
+
143
+ def trace_model(
144
+ *,
145
+ name: str | None = None,
146
+ model: str | None = None,
147
+ root: bool = False,
148
+ agent_name: str | None = None,
149
+ ) -> Callable[[F], F]:
150
+ """Convenience decorator for model-call-like functions."""
151
+
152
+ return traceable(name=name, kind="model", model=model, root=root, agent_name=agent_name)
153
+
154
+
155
+ def trace_tool(
156
+ *, name: str | None = None, root: bool = False, agent_name: str | None = None
157
+ ) -> Callable[[F], F]:
158
+ """Convenience decorator for tool-call-like functions."""
159
+
160
+ return traceable(name=name, kind="tool", root=root, agent_name=agent_name)
agentloop/demo.py ADDED
@@ -0,0 +1,283 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from pathlib import Path
6
+
7
+ from agentloop import trace_agent, trace_model_call, trace_retry, trace_tool_call
8
+
9
+ SYSTEM_LONG = (
10
+ "You are a careful research agent. Use evidence, verify claims, and write concise reports. "
11
+ * 20
12
+ )
13
+ SYSTEM_SHORT = (
14
+ "You are a careful research agent. Use evidence, verify claims, and write concise reports. " * 4
15
+ )
16
+
17
+
18
+ def _demo_metadata(**values: object) -> dict[str, object]:
19
+ metadata: dict[str, object] = {"synthetic": True, "source": "agentloop_demo"}
20
+ metadata.update(values)
21
+ return metadata
22
+
23
+
24
+ def sleep_ms(ms: int) -> None:
25
+ time.sleep(ms / 1000)
26
+
27
+
28
+ def fake_search(query: str) -> list[str]:
29
+ sleep_ms(300)
30
+ return [f"source-{i}-{query}" for i in range(1, 6)]
31
+
32
+
33
+ def fake_read(source: str) -> str:
34
+ sleep_ms(250)
35
+ return f"Content from {source}. " * 30
36
+
37
+
38
+ def inspect_result(result: str) -> str:
39
+ sleep_ms(120)
40
+ return f"inspected {result}"
41
+
42
+
43
+ def run_baseline(out_dir: str | Path = "runs") -> Path:
44
+ out_path = Path(out_dir) / "research_agent_baseline.json"
45
+ with trace_agent("research_agent_baseline", metadata=_demo_metadata()) as trace:
46
+ with trace_model_call(
47
+ "plan",
48
+ model="gpt-4.1",
49
+ input_text=SYSTEM_LONG + "Plan research on agent inference runtimes.",
50
+ output_tokens=220,
51
+ ):
52
+ sleep_ms(400)
53
+
54
+ with trace_tool_call("search_web", metadata={"query": "agent inference runtime"}):
55
+ sources = fake_search("agent inference runtime")
56
+
57
+ summaries = []
58
+ for source in sources:
59
+ with trace_tool_call("read_source", metadata={"source": source}):
60
+ content = fake_read(source)
61
+ with trace_model_call(
62
+ "summarize_source",
63
+ model="gpt-4.1",
64
+ input_text=SYSTEM_LONG + content,
65
+ output_tokens=350,
66
+ ):
67
+ sleep_ms(350)
68
+ summaries.append("summary for " + source)
69
+
70
+ with trace_retry("parse_failed_then_retry", metadata={"reason": "invalid json"}):
71
+ sleep_ms(250)
72
+
73
+ with trace_model_call(
74
+ "verify_claims",
75
+ model="gpt-4.1",
76
+ input_text=SYSTEM_LONG + "\n".join(summaries),
77
+ output_tokens=450,
78
+ ):
79
+ sleep_ms(450)
80
+
81
+ with trace_model_call(
82
+ "write_report",
83
+ model="gpt-4.1",
84
+ input_text=SYSTEM_LONG + "\n".join(summaries),
85
+ output_tokens=800,
86
+ ):
87
+ sleep_ms(550)
88
+
89
+ trace.export_json(out_path)
90
+ trace.print_report()
91
+ return out_path
92
+
93
+
94
+ def run_proof_pair(out_dir: str | Path = "runs") -> tuple[Path, Path]:
95
+ """Generate a before/after trace pair for the closed-loop product demo."""
96
+
97
+ return run_proof_baseline(out_dir), run_proof_candidate(out_dir)
98
+
99
+
100
+ def run_proof_baseline(out_dir: str | Path = "runs") -> Path:
101
+ out_path = Path(out_dir) / "agentloop_proof_baseline.json"
102
+ repeated_context = SYSTEM_LONG + "Classify and summarize every source independently. " * 20
103
+ with trace_agent(
104
+ "agentloop_proof_baseline",
105
+ metadata=_demo_metadata(schema_valid=False, quality_score=0.82),
106
+ ) as trace:
107
+ with trace_model_call(
108
+ "classify_intent",
109
+ model="gpt-4.1",
110
+ input_text="Classify this simple support request.",
111
+ output_tokens=40,
112
+ ):
113
+ sleep_ms(220)
114
+
115
+ with trace_tool_call("search_web", metadata={"query": "agent workflow profiler"}):
116
+ sources = fake_search("agent workflow profiler")[:3]
117
+
118
+ summaries = []
119
+ for source in sources:
120
+ with trace_tool_call("fake_read", metadata={"source": source}):
121
+ fake_read(source)
122
+
123
+ for index in range(15):
124
+ with trace_model_call(
125
+ "summarize_source",
126
+ model="gpt-4.1-mini",
127
+ input_text=repeated_context + f" item {index}",
128
+ input_tokens=80,
129
+ output_tokens=60,
130
+ ):
131
+ sleep_ms(12)
132
+ summaries.append(f"summary {index}")
133
+
134
+ for index in range(8):
135
+ with trace_tool_call("plan_next", metadata={"iteration": index}):
136
+ sleep_ms(60)
137
+
138
+ for name in ["search_web", "inspect_result", "search_web", "inspect_result"]:
139
+ with trace_tool_call(name):
140
+ if name == "search_web":
141
+ fake_search("same query")
142
+ else:
143
+ inspect_result("same result")
144
+
145
+ with trace_retry("invalid_json_retry", metadata={"reason": "schema_validation_failed"}):
146
+ sleep_ms(180)
147
+
148
+ large_context = repeated_context + "\n".join(summaries) + (" Evidence chunk. " * 1600)
149
+ with trace_model_call(
150
+ "final_answer",
151
+ model="gpt-4.1",
152
+ input_text=large_context,
153
+ input_tokens=4000,
154
+ output_tokens=700,
155
+ ):
156
+ sleep_ms(500)
157
+
158
+ trace.export_json(out_path)
159
+ trace.print_report()
160
+ return out_path
161
+
162
+
163
+ def run_proof_candidate(out_dir: str | Path = "runs") -> Path:
164
+ out_path = Path(out_dir) / "agentloop_proof_candidate.json"
165
+ compact_context = (
166
+ SYSTEM_SHORT + "Use cached instructions, batch summaries, and stop after convergence."
167
+ )
168
+ with trace_agent(
169
+ "agentloop_proof_candidate",
170
+ metadata=_demo_metadata(schema_valid=True, quality_score=0.94),
171
+ ) as trace:
172
+ with trace_model_call(
173
+ "classify_intent",
174
+ model="gpt-4.1-mini",
175
+ input_text="Classify this simple support request.",
176
+ output_tokens=30,
177
+ ):
178
+ sleep_ms(90)
179
+
180
+ with trace_tool_call("search_web", metadata={"query": "agent workflow profiler"}):
181
+ sources = fake_search("agent workflow profiler")[:3]
182
+
183
+ with trace_tool_call("fake_read_parallel", metadata={"count": len(sources)}):
184
+ with ThreadPoolExecutor(max_workers=3) as pool:
185
+ contents = list(pool.map(fake_read, sources))
186
+
187
+ with trace_model_call(
188
+ "summarize_batch",
189
+ model="gpt-4.1-mini",
190
+ input_text=compact_context + "\n".join(content[:220] for content in contents),
191
+ output_tokens=280,
192
+ ):
193
+ sleep_ms(170)
194
+
195
+ with trace_tool_call(
196
+ "plan_next", metadata={"iteration": 1, "stop_reason": "state_converged"}
197
+ ):
198
+ sleep_ms(60)
199
+
200
+ with trace_model_call(
201
+ "final_answer",
202
+ model="gpt-4.1-mini",
203
+ input_text=compact_context + " Compressed evidence with source ids.",
204
+ output_tokens=420,
205
+ ):
206
+ sleep_ms(220)
207
+
208
+ trace.export_json(out_path)
209
+ trace.print_report()
210
+ return out_path
211
+
212
+
213
+ def run_optimized(out_dir: str | Path = "runs") -> Path:
214
+ out_path = Path(out_dir) / "research_agent_optimized.json"
215
+ with trace_agent("research_agent_optimized", metadata=_demo_metadata()) as trace:
216
+ with trace_model_call(
217
+ "plan",
218
+ model="gpt-4.1-mini",
219
+ input_text=SYSTEM_SHORT + "Plan research on agent inference runtimes.",
220
+ output_tokens=120,
221
+ ):
222
+ sleep_ms(200)
223
+
224
+ with trace_tool_call("search_web", metadata={"query": "agent inference runtime"}):
225
+ sources = fake_search("agent inference runtime")
226
+
227
+ with trace_tool_call("read_sources_parallel", metadata={"count": len(sources)}):
228
+ with ThreadPoolExecutor(max_workers=5) as pool:
229
+ contents = list(pool.map(fake_read, sources))
230
+
231
+ compact_context = "\n".join(content[:300] for content in contents)
232
+ with trace_model_call(
233
+ "summarize_batch",
234
+ model="gpt-4.1-mini",
235
+ input_text=SYSTEM_SHORT + compact_context,
236
+ output_tokens=500,
237
+ ):
238
+ sleep_ms(300)
239
+
240
+ with trace_model_call(
241
+ "verify_and_write",
242
+ model="gpt-4.1",
243
+ input_text=SYSTEM_SHORT + compact_context,
244
+ output_tokens=650,
245
+ ):
246
+ sleep_ms(450)
247
+
248
+ trace.export_json(out_path)
249
+ trace.print_report()
250
+ return out_path
251
+
252
+
253
+ def run_langgraph_style(out_dir: str | Path = "runs") -> Path:
254
+ from agentloop.integrations.langgraph import trace_node
255
+
256
+ @trace_node("retrieve_sources")
257
+ def retrieve_sources(state: dict) -> dict:
258
+ sleep_ms(250)
259
+ state["sources"] = ["paper-a", "paper-b", "paper-c"]
260
+ return state
261
+
262
+ @trace_node("rank_sources")
263
+ def rank_sources(state: dict) -> dict:
264
+ sleep_ms(180)
265
+ state["sources"] = state["sources"][:2]
266
+ return state
267
+
268
+ out_path = Path(out_dir) / "langgraph_style_demo.json"
269
+ with trace_agent("langgraph_style_demo", metadata=_demo_metadata()) as trace:
270
+ state = {"question": "How should agent runtimes be profiled?"}
271
+ with trace_model_call(
272
+ "planner", model="gpt-4.1-mini", input_text=state["question"], output_tokens=120
273
+ ):
274
+ sleep_ms(160)
275
+ state = retrieve_sources(state)
276
+ state = rank_sources(state)
277
+ with trace_model_call(
278
+ "final_answer", model="gpt-4.1-mini", input_text=str(state), output_tokens=300
279
+ ):
280
+ sleep_ms(240)
281
+ trace.export_json(out_path)
282
+ trace.print_report()
283
+ return out_path