computer-agent-py 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.
- computer_agent_py-0.1.0.dist-info/METADATA +307 -0
- computer_agent_py-0.1.0.dist-info/RECORD +25 -0
- computer_agent_py-0.1.0.dist-info/WHEEL +4 -0
- computeragent/__init__.py +90 -0
- computeragent/_proxy/__init__.py +8 -0
- computeragent/_proxy/client.py +225 -0
- computeragent/_proxy/query.py +165 -0
- computeragent/policy/__init__.py +59 -0
- computeragent/policy/authorizer.py +161 -0
- computeragent/policy/cedar.py +182 -0
- computeragent/policy/opa.py +124 -0
- computeragent/policy/types.py +121 -0
- computeragent/py.typed +0 -0
- computeragent/telemetry/__init__.py +24 -0
- computeragent/telemetry/config.py +176 -0
- computeragent/telemetry/event.py +355 -0
- computeragent/telemetry/middleware/__init__.py +8 -0
- computeragent/telemetry/middleware/guardrails.py +127 -0
- computeragent/telemetry/middleware/pii.py +158 -0
- computeragent/telemetry/pipeline.py +160 -0
- computeragent/telemetry/sinks/__init__.py +28 -0
- computeragent/telemetry/sinks/agentos.py +442 -0
- computeragent/telemetry/sinks/message_archive.py +136 -0
- computeragent/telemetry/sinks/otel.py +375 -0
- computeragent/types.py +13 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""``OtelSink`` — emit GenAI-semconv-compliant OpenTelemetry spans + metrics.
|
|
2
|
+
|
|
3
|
+
Span tree matches the TS ``@computeragent/observability`` package so polyglot
|
|
4
|
+
deployments produce one coherent trace tree in the same backend::
|
|
5
|
+
|
|
6
|
+
invoke_agent <agent_name> [gen_ai.conversation.id = session_id,
|
|
7
|
+
gen_ai.system = "claude-agent-sdk",
|
|
8
|
+
gen_ai.agent.name = ...]
|
|
9
|
+
└── chat <model> [gen_ai.request.model = ...]
|
|
10
|
+
└── execute_tool <name> [gen_ai.tool.name = ...,
|
|
11
|
+
gen_ai.tool.call.id = ...]
|
|
12
|
+
|
|
13
|
+
Metrics::
|
|
14
|
+
|
|
15
|
+
gen_ai.client.token.usage (histogram, unit "{token}")
|
|
16
|
+
dims: gen_ai.operation.name, gen_ai.request.model, gen_ai.token.type
|
|
17
|
+
gen_ai.client.operation.duration (histogram, unit "s")
|
|
18
|
+
computeragent.usage.cost_usd (histogram, unit "{usd}") — custom
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import contextlib
|
|
24
|
+
import logging
|
|
25
|
+
from typing import TYPE_CHECKING, Any
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from opentelemetry import metrics, trace # noqa: F401 - probe imports only
|
|
29
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
|
|
30
|
+
OTLPMetricExporter as HttpMetricExporter,
|
|
31
|
+
)
|
|
32
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
33
|
+
OTLPSpanExporter as HttpSpanExporter,
|
|
34
|
+
)
|
|
35
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
36
|
+
from opentelemetry.sdk.metrics.export import (
|
|
37
|
+
ConsoleMetricExporter,
|
|
38
|
+
PeriodicExportingMetricReader,
|
|
39
|
+
)
|
|
40
|
+
from opentelemetry.sdk.resources import Resource
|
|
41
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
42
|
+
from opentelemetry.sdk.trace.export import (
|
|
43
|
+
BatchSpanProcessor,
|
|
44
|
+
ConsoleSpanExporter,
|
|
45
|
+
SimpleSpanProcessor,
|
|
46
|
+
)
|
|
47
|
+
except ImportError as _exc: # pragma: no cover - guarded by lazy export
|
|
48
|
+
raise ImportError(
|
|
49
|
+
"OtelSink requires the [otel] extra. Install with: pip install 'computer-agent-py[otel]'"
|
|
50
|
+
) from _exc
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from opentelemetry.trace import Span
|
|
54
|
+
|
|
55
|
+
from ..event import TelemetryEvent
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger("computeragent.telemetry")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# GenAI semconv attribute keys — pinned literals to guarantee parity with the
|
|
61
|
+
# TS @computeragent/observability emission. The semconv Python package's
|
|
62
|
+
# constants for these keys move between alpha versions; literals are safer.
|
|
63
|
+
GEN_AI_SYSTEM = "gen_ai.system"
|
|
64
|
+
GEN_AI_AGENT_NAME = "gen_ai.agent.name"
|
|
65
|
+
GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id"
|
|
66
|
+
GEN_AI_REQUEST_MODEL = "gen_ai.request.model"
|
|
67
|
+
GEN_AI_OPERATION_NAME = "gen_ai.operation.name"
|
|
68
|
+
GEN_AI_TOOL_NAME = "gen_ai.tool.name"
|
|
69
|
+
GEN_AI_TOOL_CALL_ID = "gen_ai.tool.call.id"
|
|
70
|
+
GEN_AI_TOKEN_TYPE = "gen_ai.token.type"
|
|
71
|
+
GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"
|
|
72
|
+
GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
|
|
73
|
+
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read_input_tokens"
|
|
74
|
+
GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS = "gen_ai.usage.cache_creation_input_tokens"
|
|
75
|
+
|
|
76
|
+
METRIC_TOKEN_USAGE = "gen_ai.client.token.usage"
|
|
77
|
+
METRIC_OPERATION_DURATION = "gen_ai.client.operation.duration"
|
|
78
|
+
METRIC_COST_USD = "computeragent.usage.cost_usd"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class OtelSink:
|
|
82
|
+
"""Translate :class:`TelemetryEvent` into OTel spans + metric measurements.
|
|
83
|
+
|
|
84
|
+
Configures its own ``TracerProvider`` and ``MeterProvider`` on first use
|
|
85
|
+
unless ``tracer_provider`` / ``meter_provider`` is passed in (tests use
|
|
86
|
+
this hook to attach in-memory exporters).
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
service_name: str | None = None,
|
|
93
|
+
exporter: str | None = None,
|
|
94
|
+
endpoint: str | None = None,
|
|
95
|
+
headers: dict[str, str] | None = None,
|
|
96
|
+
tracer_provider: Any | None = None,
|
|
97
|
+
meter_provider: Any | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
# Read effective config from the module-level resolver if no overrides.
|
|
100
|
+
from ..config import get_config
|
|
101
|
+
|
|
102
|
+
cfg = get_config()
|
|
103
|
+
self._service_name = service_name or cfg.get("service_name") or "computeragent"
|
|
104
|
+
self._exporter = (
|
|
105
|
+
exporter or cfg.get("exporter") or _infer_exporter(endpoint or cfg.get("endpoint"))
|
|
106
|
+
)
|
|
107
|
+
self._endpoint = endpoint or cfg.get("endpoint")
|
|
108
|
+
self._headers = headers or cfg.get("headers") or {}
|
|
109
|
+
|
|
110
|
+
self._tracer_provider = tracer_provider or _build_tracer_provider(
|
|
111
|
+
service_name=self._service_name,
|
|
112
|
+
exporter=self._exporter,
|
|
113
|
+
endpoint=self._endpoint,
|
|
114
|
+
headers=self._headers,
|
|
115
|
+
)
|
|
116
|
+
self._meter_provider = meter_provider or _build_meter_provider(
|
|
117
|
+
service_name=self._service_name,
|
|
118
|
+
exporter=self._exporter,
|
|
119
|
+
endpoint=self._endpoint,
|
|
120
|
+
headers=self._headers,
|
|
121
|
+
)
|
|
122
|
+
self._tracer = self._tracer_provider.get_tracer("computeragent")
|
|
123
|
+
self._meter = self._meter_provider.get_meter("computeragent")
|
|
124
|
+
|
|
125
|
+
self._token_usage_hist = self._meter.create_histogram(
|
|
126
|
+
METRIC_TOKEN_USAGE, unit="{token}", description="GenAI token usage"
|
|
127
|
+
)
|
|
128
|
+
self._duration_hist = self._meter.create_histogram(
|
|
129
|
+
METRIC_OPERATION_DURATION, unit="s", description="GenAI operation duration"
|
|
130
|
+
)
|
|
131
|
+
self._cost_hist = self._meter.create_histogram(
|
|
132
|
+
METRIC_COST_USD, unit="{usd}", description="Inference cost in USD"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# session_id → (invoke_agent span, chat span, active execute_tool span)
|
|
136
|
+
# Spans are held open across events for the same session/turn.
|
|
137
|
+
self._invoke_spans: dict[str, Span] = {}
|
|
138
|
+
self._chat_spans: dict[str, Span] = {}
|
|
139
|
+
self._tool_spans: dict[str, dict[str, Span]] = {} # session → tool_use_id → span
|
|
140
|
+
|
|
141
|
+
# ── lifecycle ----------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
async def emit(self, event: TelemetryEvent) -> None:
|
|
144
|
+
handler = _DISPATCH.get(event.kind)
|
|
145
|
+
if handler is None:
|
|
146
|
+
return
|
|
147
|
+
handler(self, event)
|
|
148
|
+
|
|
149
|
+
async def flush(self, timeout: float = 5.0) -> None:
|
|
150
|
+
# OTel SDK's flush takes milliseconds. Convert + fan out.
|
|
151
|
+
ms = int(timeout * 1000)
|
|
152
|
+
try:
|
|
153
|
+
self._tracer_provider.force_flush(timeout_millis=ms)
|
|
154
|
+
except Exception: # noqa: BLE001
|
|
155
|
+
logger.debug("tracer_provider.force_flush failed", exc_info=True)
|
|
156
|
+
try:
|
|
157
|
+
self._meter_provider.force_flush(timeout_millis=ms)
|
|
158
|
+
except Exception: # noqa: BLE001
|
|
159
|
+
logger.debug("meter_provider.force_flush failed", exc_info=True)
|
|
160
|
+
|
|
161
|
+
async def aclose(self) -> None:
|
|
162
|
+
await self.flush()
|
|
163
|
+
try:
|
|
164
|
+
self._tracer_provider.shutdown()
|
|
165
|
+
except Exception: # noqa: BLE001
|
|
166
|
+
logger.debug("tracer_provider.shutdown failed", exc_info=True)
|
|
167
|
+
try:
|
|
168
|
+
self._meter_provider.shutdown()
|
|
169
|
+
except Exception: # noqa: BLE001
|
|
170
|
+
logger.debug("meter_provider.shutdown failed", exc_info=True)
|
|
171
|
+
|
|
172
|
+
# ── handlers (one per event kind) --------------------------------------
|
|
173
|
+
|
|
174
|
+
def _on_session_started(self, event: TelemetryEvent) -> None:
|
|
175
|
+
span = self._tracer.start_span(name=f"invoke_agent {event.agent_name or 'agent'}")
|
|
176
|
+
span.set_attribute(GEN_AI_SYSTEM, "claude-agent-sdk")
|
|
177
|
+
if event.agent_name:
|
|
178
|
+
span.set_attribute(GEN_AI_AGENT_NAME, event.agent_name)
|
|
179
|
+
span.set_attribute(GEN_AI_CONVERSATION_ID, event.session_id)
|
|
180
|
+
span.set_attribute(GEN_AI_OPERATION_NAME, "invoke_agent")
|
|
181
|
+
if event.payload.get("model"):
|
|
182
|
+
span.set_attribute(GEN_AI_REQUEST_MODEL, str(event.payload["model"]))
|
|
183
|
+
self._invoke_spans[event.session_id] = span
|
|
184
|
+
|
|
185
|
+
def _on_assistant_message(self, event: TelemetryEvent) -> None:
|
|
186
|
+
# Open or reuse a chat span; spec models chat as one span per LLM call,
|
|
187
|
+
# but the upstream stream gives us one AssistantMessage per LLM call,
|
|
188
|
+
# so open/close per event.
|
|
189
|
+
chat = self._tracer.start_span(name=f"chat {event.payload.get('model') or 'unknown'}")
|
|
190
|
+
chat.set_attribute(GEN_AI_OPERATION_NAME, "chat")
|
|
191
|
+
if event.payload.get("model"):
|
|
192
|
+
chat.set_attribute(GEN_AI_REQUEST_MODEL, str(event.payload["model"]))
|
|
193
|
+
chat.set_attribute(GEN_AI_CONVERSATION_ID, event.session_id)
|
|
194
|
+
chat.end()
|
|
195
|
+
|
|
196
|
+
def _on_tool_use(self, event: TelemetryEvent) -> None:
|
|
197
|
+
tool_name = str(event.payload.get("tool_name") or "tool")
|
|
198
|
+
span = self._tracer.start_span(name=f"execute_tool {tool_name}")
|
|
199
|
+
span.set_attribute(GEN_AI_OPERATION_NAME, "execute_tool")
|
|
200
|
+
span.set_attribute(GEN_AI_TOOL_NAME, tool_name)
|
|
201
|
+
call_id = str(event.payload.get("tool_use_id") or "")
|
|
202
|
+
if call_id:
|
|
203
|
+
span.set_attribute(GEN_AI_TOOL_CALL_ID, call_id)
|
|
204
|
+
span.set_attribute(GEN_AI_CONVERSATION_ID, event.session_id)
|
|
205
|
+
self._tool_spans.setdefault(event.session_id, {})[call_id] = span
|
|
206
|
+
|
|
207
|
+
def _on_tool_result(self, event: TelemetryEvent) -> None:
|
|
208
|
+
call_id = str(event.payload.get("tool_use_id") or "")
|
|
209
|
+
span = self._tool_spans.get(event.session_id, {}).pop(call_id, None)
|
|
210
|
+
if span is None:
|
|
211
|
+
return
|
|
212
|
+
if event.payload.get("is_error"):
|
|
213
|
+
span.set_attribute("error.type", "tool_error")
|
|
214
|
+
span.end()
|
|
215
|
+
|
|
216
|
+
def _on_usage_snapshot(self, event: TelemetryEvent) -> None:
|
|
217
|
+
usage = event.payload.get("usage") or {}
|
|
218
|
+
model = event.payload.get("model")
|
|
219
|
+
common_dims = {
|
|
220
|
+
GEN_AI_OPERATION_NAME: "chat",
|
|
221
|
+
**({GEN_AI_REQUEST_MODEL: str(model)} if model else {}),
|
|
222
|
+
}
|
|
223
|
+
for token_type, key in (
|
|
224
|
+
("input", "input_tokens"),
|
|
225
|
+
("output", "output_tokens"),
|
|
226
|
+
("cache_read", "cache_read_input_tokens"),
|
|
227
|
+
("cache_creation", "cache_creation_input_tokens"),
|
|
228
|
+
):
|
|
229
|
+
raw = usage.get(key)
|
|
230
|
+
if raw is None:
|
|
231
|
+
continue
|
|
232
|
+
try:
|
|
233
|
+
v = int(raw)
|
|
234
|
+
except (TypeError, ValueError):
|
|
235
|
+
continue
|
|
236
|
+
self._token_usage_hist.record(
|
|
237
|
+
v, attributes={**common_dims, GEN_AI_TOKEN_TYPE: token_type}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
cost = event.payload.get("total_cost_usd")
|
|
241
|
+
if cost is not None:
|
|
242
|
+
with contextlib.suppress(TypeError, ValueError):
|
|
243
|
+
self._cost_hist.record(float(cost), attributes=common_dims)
|
|
244
|
+
|
|
245
|
+
def _on_session_ended(self, event: TelemetryEvent) -> None:
|
|
246
|
+
# Close any tool spans still open for this session (defensive — should
|
|
247
|
+
# already be closed by tool_result events).
|
|
248
|
+
for span in self._tool_spans.pop(event.session_id, {}).values():
|
|
249
|
+
try:
|
|
250
|
+
span.end()
|
|
251
|
+
except Exception: # noqa: BLE001
|
|
252
|
+
logger.debug("stray tool span end failed", exc_info=True)
|
|
253
|
+
|
|
254
|
+
invoke = self._invoke_spans.pop(event.session_id, None)
|
|
255
|
+
if invoke is None:
|
|
256
|
+
return
|
|
257
|
+
if event.payload.get("is_error"):
|
|
258
|
+
invoke.set_attribute(
|
|
259
|
+
"error.type",
|
|
260
|
+
str(event.payload.get("error_type") or event.payload.get("subtype") or "error"),
|
|
261
|
+
)
|
|
262
|
+
usage = event.payload.get("usage") or {}
|
|
263
|
+
for key, attr in (
|
|
264
|
+
("input_tokens", GEN_AI_USAGE_INPUT_TOKENS),
|
|
265
|
+
("output_tokens", GEN_AI_USAGE_OUTPUT_TOKENS),
|
|
266
|
+
("cache_read_input_tokens", GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS),
|
|
267
|
+
("cache_creation_input_tokens", GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS),
|
|
268
|
+
):
|
|
269
|
+
v = usage.get(key)
|
|
270
|
+
if isinstance(v, int):
|
|
271
|
+
invoke.set_attribute(attr, v)
|
|
272
|
+
invoke.end()
|
|
273
|
+
duration_s = float(event.payload.get("duration_ms", 0.0) or 0.0) / 1000.0
|
|
274
|
+
if duration_s > 0:
|
|
275
|
+
self._duration_hist.record(
|
|
276
|
+
duration_s, attributes={GEN_AI_OPERATION_NAME: "invoke_agent"}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def _on_user_message(self, _event: TelemetryEvent) -> None:
|
|
280
|
+
# No span; user messages are inputs, recorded as conversation events
|
|
281
|
+
# on the active invoke_agent span in a future iteration.
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
def _on_system_message(self, _event: TelemetryEvent) -> None:
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
def _on_policy_decision(self, event: TelemetryEvent) -> None:
|
|
288
|
+
# Annotate the active execute_tool span for this session, if any.
|
|
289
|
+
# Otherwise drop a span event onto the invoke_agent span so the
|
|
290
|
+
# decision is still visible in the trace.
|
|
291
|
+
attrs: dict[str, Any] = {
|
|
292
|
+
"policy.engine": event.payload.get("engine", ""),
|
|
293
|
+
"policy.decision": event.payload.get("decision", ""),
|
|
294
|
+
"policy.reason": event.payload.get("reason", ""),
|
|
295
|
+
"policy.latency_ms": float(event.payload.get("latency_ms", 0.0) or 0.0),
|
|
296
|
+
"policy.tool_name": event.payload.get("tool_name", ""),
|
|
297
|
+
}
|
|
298
|
+
tool_name = str(event.payload.get("tool_name") or "")
|
|
299
|
+
tool_spans = self._tool_spans.get(event.session_id, {})
|
|
300
|
+
# Tool spans are keyed by tool_use_id, which we don't have here. Pick
|
|
301
|
+
# the most-recently-opened span with a matching tool name.
|
|
302
|
+
target_span = None
|
|
303
|
+
for span in tool_spans.values():
|
|
304
|
+
if getattr(span, "name", "").endswith(tool_name) and tool_name:
|
|
305
|
+
target_span = span
|
|
306
|
+
break
|
|
307
|
+
if target_span is None:
|
|
308
|
+
target_span = self._invoke_spans.get(event.session_id)
|
|
309
|
+
if target_span is not None:
|
|
310
|
+
try:
|
|
311
|
+
target_span.add_event("policy_decision", attributes=attrs)
|
|
312
|
+
except Exception: # noqa: BLE001
|
|
313
|
+
logger.debug("policy_decision span event failed", exc_info=True)
|
|
314
|
+
return
|
|
315
|
+
# No active span — emit a standalone span so the decision is captured.
|
|
316
|
+
try:
|
|
317
|
+
span = self._tracer.start_span(name=f"policy_decision {tool_name or 'tool'}")
|
|
318
|
+
for k, v in attrs.items():
|
|
319
|
+
span.set_attribute(k, v)
|
|
320
|
+
span.set_attribute(GEN_AI_CONVERSATION_ID, event.session_id)
|
|
321
|
+
span.end()
|
|
322
|
+
except Exception: # noqa: BLE001
|
|
323
|
+
logger.debug("policy_decision standalone span failed", exc_info=True)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# Map kind → instance method. Resolved at module import; no per-event isinstance.
|
|
327
|
+
_DISPATCH: dict[str, Any] = {
|
|
328
|
+
"session_started": OtelSink._on_session_started,
|
|
329
|
+
"assistant_message": OtelSink._on_assistant_message,
|
|
330
|
+
"tool_use": OtelSink._on_tool_use,
|
|
331
|
+
"tool_result": OtelSink._on_tool_result,
|
|
332
|
+
"usage_snapshot": OtelSink._on_usage_snapshot,
|
|
333
|
+
"session_ended": OtelSink._on_session_ended,
|
|
334
|
+
"user_message": OtelSink._on_user_message,
|
|
335
|
+
"system_message": OtelSink._on_system_message,
|
|
336
|
+
"policy_decision": OtelSink._on_policy_decision,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _infer_exporter(endpoint: str | None) -> str:
|
|
341
|
+
return "otlp-http" if endpoint else "console"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _build_tracer_provider(
|
|
345
|
+
*,
|
|
346
|
+
service_name: str,
|
|
347
|
+
exporter: str,
|
|
348
|
+
endpoint: str | None,
|
|
349
|
+
headers: dict[str, str],
|
|
350
|
+
) -> TracerProvider:
|
|
351
|
+
resource = Resource.create({"service.name": service_name})
|
|
352
|
+
provider = TracerProvider(resource=resource)
|
|
353
|
+
if exporter == "otlp-http":
|
|
354
|
+
span_exporter = HttpSpanExporter(endpoint=endpoint, headers=headers or None)
|
|
355
|
+
provider.add_span_processor(BatchSpanProcessor(span_exporter))
|
|
356
|
+
else:
|
|
357
|
+
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
|
|
358
|
+
return provider
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _build_meter_provider(
|
|
362
|
+
*,
|
|
363
|
+
service_name: str,
|
|
364
|
+
exporter: str,
|
|
365
|
+
endpoint: str | None,
|
|
366
|
+
headers: dict[str, str],
|
|
367
|
+
) -> MeterProvider:
|
|
368
|
+
resource = Resource.create({"service.name": service_name})
|
|
369
|
+
metric_exporter: Any
|
|
370
|
+
if exporter == "otlp-http":
|
|
371
|
+
metric_exporter = HttpMetricExporter(endpoint=endpoint, headers=headers or None)
|
|
372
|
+
else:
|
|
373
|
+
metric_exporter = ConsoleMetricExporter()
|
|
374
|
+
reader = PeriodicExportingMetricReader(metric_exporter, export_interval_millis=5000)
|
|
375
|
+
return MeterProvider(resource=resource, metric_readers=[reader])
|
computeragent/types.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Re-export of ``claude_agent_sdk.types``.
|
|
2
|
+
|
|
3
|
+
Every type that ``claude_agent_sdk.types`` exposes is re-exported here with
|
|
4
|
+
identity preserved. This is what makes ``isinstance(msg, ResultMessage)`` work
|
|
5
|
+
whether the user imported ``ResultMessage`` from ``claude_agent_sdk.types`` or
|
|
6
|
+
from ``computeragent.types``.
|
|
7
|
+
|
|
8
|
+
Switching the import line is the only change a drop-in user needs.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from claude_agent_sdk.types import * # noqa: F401, F403
|