@utopia-ai/cli 0.1.0
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.
- package/.claude/settings.json +1 -0
- package/.claude/settings.local.json +38 -0
- package/bin/utopia.js +20 -0
- package/package.json +46 -0
- package/python/README.md +34 -0
- package/python/instrumenter/instrument.py +1148 -0
- package/python/pyproject.toml +32 -0
- package/python/setup.py +27 -0
- package/python/utopia_runtime/__init__.py +30 -0
- package/python/utopia_runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/utopia_runtime/__pycache__/client.cpython-313.pyc +0 -0
- package/python/utopia_runtime/__pycache__/probe.cpython-313.pyc +0 -0
- package/python/utopia_runtime/client.py +31 -0
- package/python/utopia_runtime/probe.py +446 -0
- package/python/utopia_runtime.egg-info/PKG-INFO +59 -0
- package/python/utopia_runtime.egg-info/SOURCES.txt +10 -0
- package/python/utopia_runtime.egg-info/dependency_links.txt +1 -0
- package/python/utopia_runtime.egg-info/top_level.txt +1 -0
- package/scripts/publish-npm.sh +14 -0
- package/scripts/publish-pypi.sh +17 -0
- package/src/cli/commands/codex.ts +193 -0
- package/src/cli/commands/context.ts +188 -0
- package/src/cli/commands/destruct.ts +237 -0
- package/src/cli/commands/easter-eggs.ts +203 -0
- package/src/cli/commands/init.ts +505 -0
- package/src/cli/commands/instrument.ts +962 -0
- package/src/cli/commands/mcp.ts +16 -0
- package/src/cli/commands/serve.ts +194 -0
- package/src/cli/commands/status.ts +304 -0
- package/src/cli/commands/validate.ts +328 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/utils/config.ts +54 -0
- package/src/graph/index.ts +687 -0
- package/src/instrumenter/javascript.ts +1798 -0
- package/src/mcp/index.ts +886 -0
- package/src/runtime/js/index.ts +518 -0
- package/src/runtime/js/package-lock.json +30 -0
- package/src/runtime/js/package.json +30 -0
- package/src/runtime/js/tsconfig.json +16 -0
- package/src/server/db/index.ts +26 -0
- package/src/server/db/schema.ts +45 -0
- package/src/server/index.ts +79 -0
- package/src/server/middleware/auth.ts +74 -0
- package/src/server/routes/admin.ts +36 -0
- package/src/server/routes/graph.ts +358 -0
- package/src/server/routes/probes.ts +286 -0
- package/src/types.ts +147 -0
- package/src/utopia-mode/index.ts +206 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "utopia-runtime"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Zero-impact production probe runtime for Utopia — gives AI coding agents real-time visibility into how code runs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = ["utopia", "probes", "observability", "production-context", "ai-agents"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Topic :: System :: Monitoring",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/paulvann/utopia"
|
|
29
|
+
Repository = "https://github.com/paulvann/utopia"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
include = ["utopia_runtime*"]
|
package/python/setup.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='utopia-runtime',
|
|
5
|
+
version='0.1.0',
|
|
6
|
+
description='Lightweight probe runtime for Utopia - zero-impact production observability',
|
|
7
|
+
long_description='Utopia Runtime provides non-blocking, zero-dependency probe reporting '
|
|
8
|
+
'for Python applications. It collects errors, database calls, API calls, '
|
|
9
|
+
'infrastructure info, and function traces, shipping them asynchronously '
|
|
10
|
+
'to the Utopia data service via a background daemon thread.',
|
|
11
|
+
author='Utopia',
|
|
12
|
+
license='MIT',
|
|
13
|
+
packages=find_packages(),
|
|
14
|
+
python_requires='>=3.9',
|
|
15
|
+
classifiers=[
|
|
16
|
+
'Development Status :: 3 - Alpha',
|
|
17
|
+
'Intended Audience :: Developers',
|
|
18
|
+
'License :: OSI Approved :: MIT License',
|
|
19
|
+
'Programming Language :: Python :: 3',
|
|
20
|
+
'Programming Language :: Python :: 3.9',
|
|
21
|
+
'Programming Language :: Python :: 3.10',
|
|
22
|
+
'Programming Language :: Python :: 3.11',
|
|
23
|
+
'Programming Language :: Python :: 3.12',
|
|
24
|
+
'Topic :: Software Development :: Libraries',
|
|
25
|
+
'Topic :: System :: Monitoring',
|
|
26
|
+
],
|
|
27
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utopia Runtime - Lightweight probe runtime for production observability.
|
|
3
|
+
|
|
4
|
+
This package provides zero-impact probe reporting functions that collect
|
|
5
|
+
observability data (errors, database calls, API calls, infrastructure info,
|
|
6
|
+
function traces) and send them asynchronously to the Utopia data service.
|
|
7
|
+
|
|
8
|
+
Only Python stdlib is used -- no external dependencies required.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .probe import (
|
|
12
|
+
init,
|
|
13
|
+
report_error,
|
|
14
|
+
report_db,
|
|
15
|
+
report_api,
|
|
16
|
+
report_infra,
|
|
17
|
+
report_function,
|
|
18
|
+
report_llm_context,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__version__ = '0.1.0'
|
|
22
|
+
__all__ = [
|
|
23
|
+
'init',
|
|
24
|
+
'report_error',
|
|
25
|
+
'report_db',
|
|
26
|
+
'report_api',
|
|
27
|
+
'report_infra',
|
|
28
|
+
'report_function',
|
|
29
|
+
'report_llm_context',
|
|
30
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""HTTP client for sending probe data to the Utopia data service.
|
|
2
|
+
|
|
3
|
+
Uses only ``urllib`` from the standard library -- no external dependencies.
|
|
4
|
+
All public functions are designed to never raise; failures are silently
|
|
5
|
+
swallowed so that instrumented applications are never impacted by probe
|
|
6
|
+
reporting issues.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import urllib.request
|
|
11
|
+
import urllib.error
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def send_probes(endpoint: str, probes: list[dict[str, Any]]) -> bool:
|
|
16
|
+
"""Send a batch of probes to the data service. Never raises."""
|
|
17
|
+
try:
|
|
18
|
+
url = f"{endpoint.rstrip('/')}/api/v1/probes"
|
|
19
|
+
data = json.dumps(probes).encode("utf-8")
|
|
20
|
+
req = urllib.request.Request(
|
|
21
|
+
url,
|
|
22
|
+
data=data,
|
|
23
|
+
headers={
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
},
|
|
26
|
+
method="POST",
|
|
27
|
+
)
|
|
28
|
+
with urllib.request.urlopen(req, timeout=2) as resp:
|
|
29
|
+
return 200 <= resp.status < 300
|
|
30
|
+
except Exception:
|
|
31
|
+
return False
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utopia probe engine -- the core reporting module.
|
|
3
|
+
|
|
4
|
+
All ``report_*`` functions are designed to be completely non-blocking and
|
|
5
|
+
safe: they never raise exceptions, never slow down the caller, and silently
|
|
6
|
+
discard data when the circuit breaker is open or the queue is full.
|
|
7
|
+
|
|
8
|
+
Architecture:
|
|
9
|
+
* A module-level ``queue.Queue`` buffers probe dicts.
|
|
10
|
+
* A single daemon thread drains the queue every 5 s (or sooner when the
|
|
11
|
+
batch reaches 50 items) and ships them via ``client.send_probes()``.
|
|
12
|
+
* A simple circuit breaker (3 consecutive failures -> open for 60 s)
|
|
13
|
+
avoids hammering a dead endpoint.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import atexit
|
|
17
|
+
import os
|
|
18
|
+
import queue
|
|
19
|
+
import socket
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
import uuid
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
from . import client as _client
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Module-level state
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
_config: dict[str, str] = {
|
|
33
|
+
"endpoint": "",
|
|
34
|
+
"project_id": "",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_queue: queue.Queue[dict[str, Any]] = queue.Queue(maxsize=10_000)
|
|
38
|
+
|
|
39
|
+
_worker_thread: Optional[threading.Thread] = None
|
|
40
|
+
_worker_lock = threading.Lock()
|
|
41
|
+
_started = False
|
|
42
|
+
|
|
43
|
+
# Circuit breaker
|
|
44
|
+
_consecutive_failures = 0
|
|
45
|
+
_circuit_open = False
|
|
46
|
+
_circuit_open_time: float = 0.0
|
|
47
|
+
_FAILURE_THRESHOLD = 3
|
|
48
|
+
_CIRCUIT_OPEN_DURATION = 60.0 # seconds
|
|
49
|
+
|
|
50
|
+
# Flush settings
|
|
51
|
+
_FLUSH_INTERVAL = 5.0 # seconds
|
|
52
|
+
_BATCH_SIZE = 50
|
|
53
|
+
|
|
54
|
+
# Shutdown flag
|
|
55
|
+
_shutdown_event = threading.Event()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Initialisation
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def _load_from_config_file() -> None:
|
|
63
|
+
"""Try to load config from .utopia/config.json (walks up from cwd)."""
|
|
64
|
+
try:
|
|
65
|
+
import json
|
|
66
|
+
import pathlib
|
|
67
|
+
d = pathlib.Path.cwd()
|
|
68
|
+
for _ in range(10):
|
|
69
|
+
cfg_path = d / ".utopia" / "config.json"
|
|
70
|
+
if cfg_path.exists():
|
|
71
|
+
cfg = json.loads(cfg_path.read_text())
|
|
72
|
+
if not _config["endpoint"] and cfg.get("dataEndpoint"):
|
|
73
|
+
_config["endpoint"] = cfg["dataEndpoint"]
|
|
74
|
+
if not _config["project_id"] and cfg.get("projectId"):
|
|
75
|
+
_config["project_id"] = cfg["projectId"]
|
|
76
|
+
return
|
|
77
|
+
parent = d.parent
|
|
78
|
+
if parent == d:
|
|
79
|
+
break
|
|
80
|
+
d = parent
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def init(
|
|
86
|
+
endpoint: str = "",
|
|
87
|
+
project_id: str = "",
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Explicitly initialise the Utopia runtime."""
|
|
90
|
+
_config["endpoint"] = endpoint or os.environ.get("UTOPIA_ENDPOINT", "")
|
|
91
|
+
_config["project_id"] = project_id or os.environ.get("UTOPIA_PROJECT_ID", "")
|
|
92
|
+
if not _config["endpoint"] or not _config["project_id"]:
|
|
93
|
+
_load_from_config_file()
|
|
94
|
+
_start_worker()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _ensure_initialized() -> None:
|
|
98
|
+
"""Auto-initialise from env vars or .utopia/config.json."""
|
|
99
|
+
global _started
|
|
100
|
+
if _started:
|
|
101
|
+
return
|
|
102
|
+
with _worker_lock:
|
|
103
|
+
if _started:
|
|
104
|
+
return
|
|
105
|
+
if not _config["endpoint"]:
|
|
106
|
+
_config["endpoint"] = os.environ.get("UTOPIA_ENDPOINT", "")
|
|
107
|
+
if not _config["project_id"]:
|
|
108
|
+
_config["project_id"] = os.environ.get("UTOPIA_PROJECT_ID", "")
|
|
109
|
+
if not _config["endpoint"] or not _config["project_id"]:
|
|
110
|
+
_load_from_config_file()
|
|
111
|
+
_start_worker()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Background worker
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def _start_worker() -> None:
|
|
119
|
+
"""Start the daemon flush thread (idempotent)."""
|
|
120
|
+
global _worker_thread, _started
|
|
121
|
+
if _started:
|
|
122
|
+
return
|
|
123
|
+
_started = True
|
|
124
|
+
_worker_thread = threading.Thread(target=_flush_loop, name="utopia-probe-worker", daemon=True)
|
|
125
|
+
_worker_thread.start()
|
|
126
|
+
atexit.register(_shutdown)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _shutdown() -> None:
|
|
130
|
+
"""Drain remaining probes on interpreter shutdown."""
|
|
131
|
+
_shutdown_event.set()
|
|
132
|
+
# Give the worker a moment to flush
|
|
133
|
+
if _worker_thread is not None and _worker_thread.is_alive():
|
|
134
|
+
_worker_thread.join(timeout=3.0)
|
|
135
|
+
# Final emergency flush
|
|
136
|
+
_flush_batch()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _flush_loop() -> None:
|
|
140
|
+
"""Main loop for the background worker thread."""
|
|
141
|
+
while not _shutdown_event.is_set():
|
|
142
|
+
try:
|
|
143
|
+
# Wait up to FLUSH_INTERVAL, but wake early if batch is large
|
|
144
|
+
deadline = time.monotonic() + _FLUSH_INTERVAL
|
|
145
|
+
while time.monotonic() < deadline and not _shutdown_event.is_set():
|
|
146
|
+
if _queue.qsize() >= _BATCH_SIZE:
|
|
147
|
+
break
|
|
148
|
+
_shutdown_event.wait(timeout=0.25)
|
|
149
|
+
_flush_batch()
|
|
150
|
+
except Exception:
|
|
151
|
+
# Never let the worker die
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _flush_batch() -> None:
|
|
156
|
+
"""Drain the queue and send a batch to the data service."""
|
|
157
|
+
global _consecutive_failures, _circuit_open, _circuit_open_time
|
|
158
|
+
|
|
159
|
+
# Circuit breaker check
|
|
160
|
+
if _circuit_open:
|
|
161
|
+
if time.monotonic() - _circuit_open_time < _CIRCUIT_OPEN_DURATION:
|
|
162
|
+
# Discard everything while circuit is open to avoid memory growth
|
|
163
|
+
while not _queue.empty():
|
|
164
|
+
try:
|
|
165
|
+
_queue.get_nowait()
|
|
166
|
+
except queue.Empty:
|
|
167
|
+
break
|
|
168
|
+
return
|
|
169
|
+
else:
|
|
170
|
+
# Half-open: allow one attempt
|
|
171
|
+
_circuit_open = False
|
|
172
|
+
_consecutive_failures = 0
|
|
173
|
+
|
|
174
|
+
# Drain queue
|
|
175
|
+
batch: list[dict[str, Any]] = []
|
|
176
|
+
while len(batch) < 500: # cap per-flush to avoid huge payloads
|
|
177
|
+
try:
|
|
178
|
+
item = _queue.get_nowait()
|
|
179
|
+
batch.append(item)
|
|
180
|
+
except queue.Empty:
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
if not batch:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
endpoint = _config.get("endpoint", "")
|
|
187
|
+
|
|
188
|
+
if not endpoint:
|
|
189
|
+
# Nowhere to send -- silently discard
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
success = _client.send_probes(endpoint, batch)
|
|
193
|
+
|
|
194
|
+
if success:
|
|
195
|
+
_consecutive_failures = 0
|
|
196
|
+
else:
|
|
197
|
+
_consecutive_failures += 1
|
|
198
|
+
if _consecutive_failures >= _FAILURE_THRESHOLD:
|
|
199
|
+
_circuit_open = True
|
|
200
|
+
_circuit_open_time = time.monotonic()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# Helpers
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
def _generate_id() -> str:
|
|
208
|
+
"""Generate a unique probe identifier."""
|
|
209
|
+
return str(uuid.uuid4())
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _timestamp() -> str:
|
|
213
|
+
"""ISO-8601 UTC timestamp."""
|
|
214
|
+
return datetime.now(timezone.utc).isoformat()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _hostname() -> str:
|
|
218
|
+
try:
|
|
219
|
+
return socket.gethostname()
|
|
220
|
+
except Exception:
|
|
221
|
+
return "<unknown>"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _base_probe(probe_type: str, file: str, line: int, function_name: str = "") -> dict[str, Any]:
|
|
225
|
+
"""Build the common probe envelope."""
|
|
226
|
+
return {
|
|
227
|
+
"id": _generate_id(),
|
|
228
|
+
"probeType": probe_type,
|
|
229
|
+
"timestamp": _timestamp(),
|
|
230
|
+
"projectId": _config.get("project_id", ""),
|
|
231
|
+
"file": file,
|
|
232
|
+
"line": line,
|
|
233
|
+
"functionName": function_name,
|
|
234
|
+
"metadata": {
|
|
235
|
+
"runtime": "python",
|
|
236
|
+
"hostname": _hostname(),
|
|
237
|
+
"pid": os.getpid(),
|
|
238
|
+
"env": os.environ.get("UTOPIA_ENV", os.environ.get("NODE_ENV", "production")),
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _enqueue(probe: dict[str, Any]) -> None:
|
|
244
|
+
"""Push a probe dict onto the queue; drop silently if full."""
|
|
245
|
+
try:
|
|
246
|
+
_queue.put_nowait(probe)
|
|
247
|
+
except queue.Full:
|
|
248
|
+
pass # back-pressure: silently drop
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
# Public report_* functions
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def report_error(
|
|
256
|
+
file: str,
|
|
257
|
+
line: int,
|
|
258
|
+
function_name: str,
|
|
259
|
+
error_type: str,
|
|
260
|
+
message: str,
|
|
261
|
+
stack: str,
|
|
262
|
+
input_data: Optional[dict[str, str]] = None,
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Report a caught exception.
|
|
265
|
+
|
|
266
|
+
Called by the injected error-probe try/except wrappers.
|
|
267
|
+
"""
|
|
268
|
+
try:
|
|
269
|
+
_ensure_initialized()
|
|
270
|
+
probe = _base_probe("error", file, line, function_name)
|
|
271
|
+
probe["data"] = {
|
|
272
|
+
"error_type": error_type,
|
|
273
|
+
"message": message,
|
|
274
|
+
"stack": stack,
|
|
275
|
+
"input_data": input_data or {},
|
|
276
|
+
}
|
|
277
|
+
_enqueue(probe)
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def report_db(
|
|
283
|
+
file: str,
|
|
284
|
+
line: int,
|
|
285
|
+
function_name: str = "",
|
|
286
|
+
operation: str = "",
|
|
287
|
+
query: Optional[str] = None,
|
|
288
|
+
table: Optional[str] = None,
|
|
289
|
+
duration: float = 0.0,
|
|
290
|
+
row_count: Optional[int] = None,
|
|
291
|
+
connection_info: Optional[dict[str, Any]] = None,
|
|
292
|
+
params: Optional[Any] = None,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Report a database operation.
|
|
295
|
+
|
|
296
|
+
Called by the injected database-probe wrappers.
|
|
297
|
+
"""
|
|
298
|
+
try:
|
|
299
|
+
_ensure_initialized()
|
|
300
|
+
probe = _base_probe("database", file, line, function_name)
|
|
301
|
+
probe["data"] = {
|
|
302
|
+
"operation": operation,
|
|
303
|
+
"query": query,
|
|
304
|
+
"table": table,
|
|
305
|
+
"duration": duration,
|
|
306
|
+
"row_count": row_count,
|
|
307
|
+
"connection_info": connection_info or {},
|
|
308
|
+
"params": repr(params) if params is not None else None,
|
|
309
|
+
}
|
|
310
|
+
_enqueue(probe)
|
|
311
|
+
except Exception:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def report_api(
|
|
316
|
+
file: str,
|
|
317
|
+
line: int,
|
|
318
|
+
function_name: str = "",
|
|
319
|
+
method: str = "",
|
|
320
|
+
url: str = "",
|
|
321
|
+
status_code: Optional[int] = None,
|
|
322
|
+
duration: float = 0.0,
|
|
323
|
+
request_headers: Optional[dict[str, str]] = None,
|
|
324
|
+
response_headers: Optional[dict[str, str]] = None,
|
|
325
|
+
request_body: Optional[str] = None,
|
|
326
|
+
response_body: Optional[str] = None,
|
|
327
|
+
error: Optional[str] = None,
|
|
328
|
+
) -> None:
|
|
329
|
+
"""Report an outbound HTTP API call.
|
|
330
|
+
|
|
331
|
+
Called by the injected API-probe wrappers.
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
_ensure_initialized()
|
|
335
|
+
probe = _base_probe("api", file, line, function_name)
|
|
336
|
+
|
|
337
|
+
# Sanitize headers: strip sensitive values
|
|
338
|
+
_sensitive_keys = {"authorization", "x-api-key", "cookie", "set-cookie"}
|
|
339
|
+
|
|
340
|
+
def _sanitize_headers(headers: Optional[dict[str, str]]) -> Optional[dict[str, str]]:
|
|
341
|
+
if headers is None:
|
|
342
|
+
return None
|
|
343
|
+
return {
|
|
344
|
+
k: ("***" if k.lower() in _sensitive_keys else v)
|
|
345
|
+
for k, v in headers.items()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
probe["data"] = {
|
|
349
|
+
"method": method,
|
|
350
|
+
"url": url,
|
|
351
|
+
"status_code": status_code,
|
|
352
|
+
"duration": duration,
|
|
353
|
+
"request_headers": _sanitize_headers(request_headers),
|
|
354
|
+
"response_headers": _sanitize_headers(response_headers),
|
|
355
|
+
"request_body": request_body,
|
|
356
|
+
"response_body": response_body,
|
|
357
|
+
"error": error,
|
|
358
|
+
}
|
|
359
|
+
_enqueue(probe)
|
|
360
|
+
except Exception:
|
|
361
|
+
pass
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def report_infra(
|
|
365
|
+
file: str,
|
|
366
|
+
line: int = 0,
|
|
367
|
+
provider: str = "other",
|
|
368
|
+
region: Optional[str] = None,
|
|
369
|
+
service_type: Optional[str] = None,
|
|
370
|
+
instance_id: Optional[str] = None,
|
|
371
|
+
container_info: Optional[dict[str, Any]] = None,
|
|
372
|
+
env_vars: Optional[dict[str, str]] = None,
|
|
373
|
+
) -> None:
|
|
374
|
+
"""Report infrastructure / deployment environment information.
|
|
375
|
+
|
|
376
|
+
Typically called once at application startup from the injected infra probe.
|
|
377
|
+
"""
|
|
378
|
+
try:
|
|
379
|
+
_ensure_initialized()
|
|
380
|
+
probe = _base_probe("infra", file, line)
|
|
381
|
+
probe["data"] = {
|
|
382
|
+
"provider": provider,
|
|
383
|
+
"region": region,
|
|
384
|
+
"service_type": service_type,
|
|
385
|
+
"instance_id": instance_id,
|
|
386
|
+
"container_info": container_info or {},
|
|
387
|
+
"env_vars": env_vars or {},
|
|
388
|
+
}
|
|
389
|
+
_enqueue(probe)
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def report_function(
|
|
395
|
+
file: str,
|
|
396
|
+
line: int,
|
|
397
|
+
function_name: str,
|
|
398
|
+
args: Optional[dict[str, str]] = None,
|
|
399
|
+
return_value: Optional[str] = None,
|
|
400
|
+
duration: float = 0.0,
|
|
401
|
+
call_stack: Optional[list[str]] = None,
|
|
402
|
+
) -> None:
|
|
403
|
+
"""Report a function invocation (utopia mode).
|
|
404
|
+
|
|
405
|
+
Captures entry arguments and execution duration for every instrumented
|
|
406
|
+
function call.
|
|
407
|
+
"""
|
|
408
|
+
try:
|
|
409
|
+
_ensure_initialized()
|
|
410
|
+
probe = _base_probe("function", file, line, function_name)
|
|
411
|
+
probe["data"] = {
|
|
412
|
+
"args": args or {},
|
|
413
|
+
"return_value": return_value,
|
|
414
|
+
"duration": duration,
|
|
415
|
+
"call_stack": call_stack or [],
|
|
416
|
+
}
|
|
417
|
+
_enqueue(probe)
|
|
418
|
+
except Exception:
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def report_llm_context(
|
|
423
|
+
file: str,
|
|
424
|
+
line: int,
|
|
425
|
+
function_name: str,
|
|
426
|
+
context: Any,
|
|
427
|
+
) -> None:
|
|
428
|
+
"""Report LLM context data (utopia mode).
|
|
429
|
+
|
|
430
|
+
Used to capture context windows, prompt chains, and other LLM-specific
|
|
431
|
+
observability data that helps AI coding agents understand the production
|
|
432
|
+
environment.
|
|
433
|
+
"""
|
|
434
|
+
try:
|
|
435
|
+
_ensure_initialized()
|
|
436
|
+
probe = _base_probe("llm_context", file, line, function_name)
|
|
437
|
+
# Serialize context safely
|
|
438
|
+
if isinstance(context, dict):
|
|
439
|
+
probe["data"] = context
|
|
440
|
+
elif isinstance(context, str):
|
|
441
|
+
probe["data"] = {"context": context}
|
|
442
|
+
else:
|
|
443
|
+
probe["data"] = {"context": repr(context)}
|
|
444
|
+
_enqueue(probe)
|
|
445
|
+
except Exception:
|
|
446
|
+
pass
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: utopia-runtime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-impact production probe runtime for Utopia — gives AI coding agents real-time visibility into how code runs
|
|
5
|
+
Author: Utopia
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/paulvann/utopia
|
|
8
|
+
Project-URL: Repository, https://github.com/paulvann/utopia
|
|
9
|
+
Keywords: utopia,probes,observability,production-context,ai-agents
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
|
|
26
|
+
# utopia-runtime
|
|
27
|
+
|
|
28
|
+
Zero-impact production probe runtime for [Utopia](https://github.com/paulvann/utopia).
|
|
29
|
+
|
|
30
|
+
Captures errors, API calls, database queries, function behavior, and infrastructure context — sending it to the Utopia data service so AI coding agents can understand how your code runs in production.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install utopia-runtime
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Probes are added by `utopia instrument` — you don't typically import this directly. The runtime auto-initializes from `.utopia/config.json` in your project root.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import utopia_runtime
|
|
44
|
+
|
|
45
|
+
# Reports are non-blocking and never raise
|
|
46
|
+
utopia_runtime.report_function(
|
|
47
|
+
file="app/routes.py",
|
|
48
|
+
line=25,
|
|
49
|
+
function_name="get_user",
|
|
50
|
+
args=[{"user_id": 123}],
|
|
51
|
+
return_value={"found": True},
|
|
52
|
+
duration=15,
|
|
53
|
+
call_stack=[],
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Zero dependencies
|
|
58
|
+
|
|
59
|
+
Uses only the Python standard library. No external packages required.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
utopia_runtime/__init__.py
|
|
5
|
+
utopia_runtime/client.py
|
|
6
|
+
utopia_runtime/probe.py
|
|
7
|
+
utopia_runtime.egg-info/PKG-INFO
|
|
8
|
+
utopia_runtime.egg-info/SOURCES.txt
|
|
9
|
+
utopia_runtime.egg-info/dependency_links.txt
|
|
10
|
+
utopia_runtime.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
utopia_runtime
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
echo "Publishing utopia-runtime to npm..."
|
|
5
|
+
cd "$(dirname "$0")/../src/runtime/js"
|
|
6
|
+
|
|
7
|
+
# Build
|
|
8
|
+
npm install
|
|
9
|
+
npm run build
|
|
10
|
+
|
|
11
|
+
# Publish (use --access public for first publish of scoped packages)
|
|
12
|
+
npm publish --access public
|
|
13
|
+
|
|
14
|
+
echo "Done! Published utopia-runtime@$(node -e "console.log(require('./package.json').version)")"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
echo "Publishing utopia-runtime to PyPI..."
|
|
5
|
+
cd "$(dirname "$0")/../python"
|
|
6
|
+
|
|
7
|
+
# Clean old builds
|
|
8
|
+
rm -rf dist/ build/ *.egg-info
|
|
9
|
+
|
|
10
|
+
# Build
|
|
11
|
+
python3 -m pip install --upgrade build twine 2>/dev/null
|
|
12
|
+
python3 -m build
|
|
13
|
+
|
|
14
|
+
# Upload
|
|
15
|
+
python3 -m twine upload dist/*
|
|
16
|
+
|
|
17
|
+
echo "Done! Published utopia-runtime@$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"
|