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.
@@ -0,0 +1,307 @@
1
+ Metadata-Version: 2.3
2
+ Name: computer-agent-py
3
+ Version: 0.1.0
4
+ Summary: Drop-in replacement for claude-agent-sdk that adds a proxied telemetry pipeline (PII redaction + guardrails) with OpenTelemetry and AgentOS sinks.
5
+ Keywords: computeragent,claude-agent-sdk,claude,agent,telemetry,otel,opentelemetry,agentos,pii
6
+ Author: Abhi Bhat
7
+ Author-email: Abhi Bhat <abhishek.bhat@lyzr.ai>
8
+ License: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: claude-agent-sdk>=0.2,<0.3
21
+ Requires-Dist: typing-extensions>=4.12
22
+ Requires-Dist: motor>=3.5 ; extra == 'agentos'
23
+ Requires-Dist: opentelemetry-api>=1.27 ; extra == 'all'
24
+ Requires-Dist: opentelemetry-sdk>=1.27 ; extra == 'all'
25
+ Requires-Dist: opentelemetry-exporter-otlp>=1.27 ; extra == 'all'
26
+ Requires-Dist: opentelemetry-semantic-conventions>=0.48b0 ; extra == 'all'
27
+ Requires-Dist: motor>=3.5 ; extra == 'all'
28
+ Requires-Dist: cedarpy>=4,<5 ; extra == 'all'
29
+ Requires-Dist: cedarpy>=4,<5 ; extra == 'cedar'
30
+ Requires-Dist: opentelemetry-api>=1.27 ; extra == 'otel'
31
+ Requires-Dist: opentelemetry-sdk>=1.27 ; extra == 'otel'
32
+ Requires-Dist: opentelemetry-exporter-otlp>=1.27 ; extra == 'otel'
33
+ Requires-Dist: opentelemetry-semantic-conventions>=0.48b0 ; extra == 'otel'
34
+ Requires-Python: >=3.10
35
+ Project-URL: Changelog, https://github.com/open-gitagent/computer-agent-py/blob/main/CHANGELOG.md
36
+ Project-URL: Homepage, https://github.com/open-gitagent/computer-agent-py
37
+ Project-URL: Issues, https://github.com/open-gitagent/computer-agent-py/issues
38
+ Provides-Extra: agentos
39
+ Provides-Extra: all
40
+ Provides-Extra: cedar
41
+ Provides-Extra: otel
42
+ Description-Content-Type: text/markdown
43
+
44
+ # computer-agent-py
45
+
46
+ [![PyPI](https://img.shields.io/pypi/v/computer-agent-py.svg)](https://pypi.org/project/computer-agent-py/)
47
+ [![Python](https://img.shields.io/pypi/pyversions/computer-agent-py.svg)](https://pypi.org/project/computer-agent-py/)
48
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
49
+
50
+ **Drop-in replacement for [`claude-agent-sdk`](https://pypi.org/project/claude-agent-sdk/)** — change the import line, get a proxied telemetry pipeline with PII redaction, configurable OTel export, policy-based tool authorization, and full AgentOS integration for free.
51
+
52
+ ```diff
53
+ - from claude_agent_sdk import ClaudeAgentOptions, query
54
+ - from claude_agent_sdk.types import ResultMessage
55
+ + from computeragent import ClaudeAgentOptions, query
56
+ + from computeragent.types import ResultMessage
57
+ ```
58
+
59
+ Every other line stays identical. `isinstance(msg, ResultMessage)` still works. The `claude` CLI subprocess, AWS Bedrock auth, MCP servers, permission modes, `cwd`, `add_dirs` — all behave exactly as the upstream SDK does.
60
+
61
+ > **Package vs import name** — PyPI distribution is `computer-agent-py` (hyphens for the wheel); the import name is `computeragent` (Python doesn't allow hyphens). So `pip install computer-agent-py` then `from computeragent import …`.
62
+
63
+ ## Why use this
64
+
65
+ Adopting `computer-agent-py` in place of `claude-agent-sdk` gives you, without rewriting your agent code:
66
+
67
+ - **OpenTelemetry traces** following the GenAI Semantic Conventions, vendor-neutral (New Relic, Datadog, ClickHouse, Honeycomb, Tempo, Jaeger — one env-var change).
68
+ - **PII redaction** at the package boundary — email, phone, SSN, credit cards, AWS keys redacted before anything reaches a sink.
69
+ - **Generic guardrails** — attribute truncation, tool allowlists, per-session cost ceilings, content filters.
70
+ - **Policy-based tool-use authorization** — gate every tool call through OPA (remote) or Cedar (in-process). Fail-closed by default.
71
+ - **AgentOS visibility** — write to the same Mongo collections (`agent_registry`, `agent_logs`, `agent_messages`, `sessions`, `slack_threads`) the AgentOS frontend already reads. Library-mode agents show up in the Agents list, Logs tab, Chat transcript, and (with the harness running) live chat sandboxes.
72
+ - **Per-message archive** — every message, tool call, and policy decision archived to `agent_messages` for replay, RAG, and forensic audit.
73
+
74
+ ## Install
75
+
76
+ ```bash
77
+ pip install computer-agent-py # core drop-in + OPA policy engine
78
+ pip install 'computer-agent-py[otel]' # + OpenTelemetry sink
79
+ pip install 'computer-agent-py[agentos]' # + AgentOS Mongo sinks
80
+ pip install 'computer-agent-py[cedar]' # + Cedar policy engine (in-process)
81
+ pip install 'computer-agent-py[all]' # everything
82
+ ```
83
+
84
+ **Prerequisite** — same as upstream: the `claude` CLI binary on `PATH` and Anthropic / Bedrock credentials in the environment.
85
+
86
+ ## Quickstart
87
+
88
+ ```python
89
+ import asyncio
90
+ from computeragent import ClaudeAgentOptions, query
91
+ from computeragent.types import ResultMessage
92
+
93
+ async def main():
94
+ options = ClaudeAgentOptions(
95
+ model="claude-sonnet-4-5",
96
+ system_prompt="You are a helpful assistant.",
97
+ allowed_tools=["Read", "Glob", "Grep"],
98
+ permission_mode="bypassPermissions",
99
+ )
100
+
101
+ async for message in query(prompt="Summarize the README.md in this directory.", options=options):
102
+ if isinstance(message, ResultMessage):
103
+ print(f"answer ({message.num_turns} turns, ${message.total_cost_usd:.4f}):")
104
+ print(message.result)
105
+
106
+ asyncio.run(main())
107
+ ```
108
+
109
+ That's it — same shape as `claude-agent-sdk`. Telemetry is configured from env vars; without anything set, nothing leaves your process.
110
+
111
+ ## Configure telemetry
112
+
113
+ ### Env-driven (zero code change)
114
+
115
+ | Variable | Effect |
116
+ |---|---|
117
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | Enables OTLP/HTTP export. Unset → console exporter (debug). |
118
+ | `OTEL_EXPORTER_OTLP_HEADERS` | Comma-separated `key=value` (e.g. `api-key=NRRX-...` for New Relic). |
119
+ | `OTEL_SERVICE_NAME` | `service.name` attribute on every span. Default: `computeragent`. |
120
+ | `COMPUTERAGENT_OTEL` | `disabled` to suppress OtelSink. |
121
+ | `AGENTOS_MONGO_URL` | When set + `[agentos]` installed, attaches AgentOS sinks (registry, logs, sessions, agent_messages, slack_threads). |
122
+ | `AGENTOS_MONGO_DB` | Mongo database name. Default: `agentos`. |
123
+ | `COMPUTERAGENT_CAPTURE_CONTENT` | `1` to include prompts/responses on OTel spans. Default: off. |
124
+ | `COMPUTERAGENT_CAPTURE_CONTENT_MODE` | `events` (default) \| `attributes` \| `both`. |
125
+
126
+ ### Programmatic
127
+
128
+ ```python
129
+ from computeragent import configure, PiiRedactor, GuardrailFilter
130
+ from computeragent.telemetry.sinks import OtelSink, AgentRegistrySink, MongoMessageSink
131
+
132
+ configure(
133
+ middleware=[
134
+ PiiRedactor(strategy="hash", extra_patterns=[r"BADGE-\d{6}"]),
135
+ GuardrailFilter(
136
+ max_attribute_length=4096,
137
+ tool_name_allowlist={"Read", "Glob", "Grep", "mcp__nordassist-tools__*"},
138
+ cost_ceiling_usd=1.50,
139
+ ),
140
+ ],
141
+ sinks=[
142
+ OtelSink(), # picks up env
143
+ AgentRegistrySink(mongo_url="mongodb://..."), # registry + logs + sessions + slack_threads
144
+ MongoMessageSink(mongo_url="mongodb://..."), # per-message archive
145
+ ],
146
+ )
147
+ ```
148
+
149
+ ## Vendor-neutral OTel destinations
150
+
151
+ The package emits standard OTLP — point it at any backend by setting two env vars. No code change.
152
+
153
+ | Destination | `OTEL_EXPORTER_OTLP_ENDPOINT` | `OTEL_EXPORTER_OTLP_HEADERS` |
154
+ |---|---|---|
155
+ | **New Relic** | `https://otlp.nr-data.net` | `api-key=<NR_LICENSE_KEY>` |
156
+ | **Datadog** (via DD Agent OTLP) | `http://localhost:4318` | _(unset; agent handles auth)_ |
157
+ | **Honeycomb** | `https://api.honeycomb.io` | `x-honeycomb-team=<KEY>` |
158
+ | **Grafana Cloud Tempo** | `https://tempo-prod-...grafana.net:443` | `authorization=Basic <base64>` |
159
+ | **Self-hosted Jaeger / Tempo / SigNoz** | `http://<host>:4318` | _(unset)_ |
160
+ | **Local console (debug)** | _(unset)_ | _(unset)_ |
161
+
162
+ Full recipe table — including direct New Relic / Datadog without an OTel collector — is in [`examples/e2e/destinations.md`](examples/e2e/destinations.md).
163
+
164
+ ## Policy-based tool-use authorization
165
+
166
+ For agents that need stronger guardrails than `permission_mode`, attach an external policy engine. Activation is a single new option-field; the rest of the worker code is unchanged.
167
+
168
+ ```python
169
+ from computeragent import ClaudeAgentOptions, PolicyPrincipal, PolicyResource, query
170
+ from computeragent.policy import OpaPolicyEngine, PolicyToolAuthorizer
171
+
172
+ opa = OpaPolicyEngine(
173
+ url="http://opa.platform:8181",
174
+ policy_path="computeragent/tools/allow",
175
+ fail_mode="deny", # default — engine errors deny the call
176
+ )
177
+ authorizer = PolicyToolAuthorizer(
178
+ engine=opa,
179
+ principal_resolver=lambda ctx: PolicyPrincipal(id="alice", groups=["engineer"]),
180
+ resource_resolver=lambda ctx: PolicyResource(agent_name="nordassist", model="claude-sonnet-4-5"),
181
+ context_resolver=lambda ctx: {"env": "prod"},
182
+ )
183
+
184
+ options = ClaudeAgentOptions(
185
+ ...,
186
+ permission_mode="default", # was "bypassPermissions"
187
+ can_use_tool=authorizer,
188
+ )
189
+ ```
190
+
191
+ Swap `OpaPolicyEngine` for `CedarPolicyEngine` (install with `pip install 'computer-agent-py[cedar]'`) and the worker code is identical:
192
+
193
+ ```python
194
+ from computeragent.policy import CedarPolicyEngine
195
+
196
+ cedar = CedarPolicyEngine(
197
+ policies=open("policies/computeragent.cedar").read(),
198
+ fail_mode="deny",
199
+ )
200
+ authorizer = PolicyToolAuthorizer(engine=cedar, principal_resolver=..., ...)
201
+ ```
202
+
203
+ Sample policies are in [`examples/policies/`](examples/policies/) — one Rego file for OPA, one Cedar file. Each policy receives a canonical `PolicyInput` shape (`principal`, `action`, `resource`, `context`) so the engine choice is purely operational.
204
+
205
+ Every authorization decision emits a `policy_decision` telemetry event — `OtelSink` annotates the active `execute_tool` span with `policy.decision`, `policy.reason`, `policy.engine`, and `policy.latency_ms` so security audits and span queries co-locate.
206
+
207
+ ## AgentOS integration
208
+
209
+ When `[agentos]` is installed and `AGENTOS_MONGO_URL` is set, every agent run writes to the Mongo collections the AgentOS frontend already reads:
210
+
211
+ | Collection | Per | What's in it |
212
+ |---|---|---|
213
+ | `agent_registry` | agent name | Identity + harness + model + last-seen; idempotent upsert |
214
+ | `agent_logs` | run | Rolled-up query/reply + tokens + cost + ok/error — drives the Logs tab |
215
+ | `sessions` | session | `entries[]` of `{type, text}` chat-bubble messages — drives the Chat tab transcript |
216
+ | `slack_threads` | session | TS parity row that drives the per-agent `sessionCount` + `lastActivity` aggregates |
217
+ | `agent_messages` | message | Per-event archive (`user_message`, `assistant_message`, `tool_use`, `tool_result`, `usage_snapshot`, `policy_decision`, `system_message`) for replay, RAG, audit |
218
+
219
+ The doc shapes are byte-for-byte compatible with the TypeScript `@open-gitagent/agent-registry-mongo` package — Python-driven agents show up in the same AgentOS UI that hosted TS agents do, with no frontend change.
220
+
221
+ ### Live chat for library-mode agents
222
+
223
+ When the AgentRegistrySink writes its `agent_registry.source` row, it includes a full inline `IdentitySource` with `files: {agent.yaml, CLAUDE.md}` derived from your `ClaudeAgentOptions`. If a user clicks "New Chat" on the agent in the AgentOS SPA, the harness can clone those files into a sandbox workdir and spawn a live conversation — same UX as hosted (git-sourced) agents. The historical transcript stays available in the Chat tab regardless.
224
+
225
+ ### DocumentDB compatibility
226
+
227
+ Prod deployments on AWS DocumentDB work without changes. The sinks use only operators DocumentDB supports — `$set`, `$setOnInsert`, `$push`, `update_one(upsert=True)`, `insert_one`. No aggregation pipelines, transactions, change streams, or TTL indexes. Set `MONGO_URL` with the standard `tls=true&tlsCAFile=...` params and mount the DocumentDB CA bundle.
228
+
229
+ ## Architecture
230
+
231
+ ```
232
+ user code: from computeragent import query, ClaudeAgentOptions
233
+
234
+
235
+ computeragent._proxy.query ──┐
236
+ │ │
237
+ ▼ │ PolicyToolAuthorizer
238
+ claude_agent_sdk → claude CLI subprocess → Bedrock│ (OPA / Cedar)
239
+ │ │ via can_use_tool
240
+ ▼ │
241
+ yielded messages │
242
+ │ │
243
+ ▼ │
244
+ TelemetryPipeline (taps stream) │
245
+ │ │
246
+ ┌──── middleware ─────┐ │
247
+ │ PiiRedactor │ │
248
+ │ GuardrailFilter │ ◄────────────────────┘
249
+ │ <user-defined> │
250
+ └────────┬────────────┘
251
+
252
+ ┌──── fan-out to sinks ───────────────────────────┐
253
+ │ │
254
+ ▼ ▼ ▼ ▼
255
+ OtelSink AgentRegistrySink MongoMessageSink <user>
256
+ │ │ │
257
+ ▼ ▼ ▼
258
+ OTLP backend agent_registry agent_messages
259
+ (NR / DD / agent_logs (per-message
260
+ ClickHouse / sessions archive)
261
+ Tempo …) slack_threads
262
+ (drives AgentOS UI)
263
+ ```
264
+
265
+ The proxy is a pure tap — messages are never modified or reordered. Sinks run as background tasks so a slow exporter never stalls the agent hot path; `query()`'s `finally` block awaits them with a 5 s default timeout. Telemetry never breaks an agent run: middleware and sink exceptions are absorbed and logged.
266
+
267
+ ## Live e2e against AgentOS
268
+
269
+ [`examples/e2e/`](examples/e2e/) contains a recipe for standing up the full TypeScript stack (mongo + clickhouse + otel-collector + harness + agentos-server + SPA) via docker-compose and running this package against it. After ~60s of warm-up plus a 30s Python script run, you'll see the agent appear in the SPA's Agents list with `logCount`, `sessionCount`, `lastActivity`, and `activeSandboxes` populated; the Logs tab will show the rollup; the Chat tab will show the per-message transcript; the Observability tab will show the OTel trace tree. See [`examples/e2e/README.md`](examples/e2e/README.md).
270
+
271
+ ## Examples
272
+
273
+ | File | Demonstrates |
274
+ |---|---|
275
+ | [`examples/pdf_drop_in.py`](examples/pdf_drop_in.py) | The minimum drop-in change |
276
+ | [`examples/with_otel.py`](examples/with_otel.py) | OTel pointed at a local collector |
277
+ | [`examples/with_new_relic.py`](examples/with_new_relic.py) | OTel pointed at New Relic (just env vars) |
278
+ | [`examples/with_datadog.py`](examples/with_datadog.py) | OTel pointed at Datadog |
279
+ | [`examples/with_agentos.py`](examples/with_agentos.py) | AgentOS Mongo writes |
280
+ | [`examples/with_message_archive.py`](examples/with_message_archive.py) | Per-message archive |
281
+ | [`examples/with_pii_redaction.py`](examples/with_pii_redaction.py) | PII middleware in front of every sink |
282
+ | [`examples/with_opa_policy.py`](examples/with_opa_policy.py) | OPA-gated tool use |
283
+ | [`examples/with_cedar_policy.py`](examples/with_cedar_policy.py) | Cedar-gated tool use (in-process) |
284
+ | [`examples/multi_sink.py`](examples/multi_sink.py) | All sinks + all guardrails together |
285
+ | [`examples/e2e/run_live_demo.py`](examples/e2e/run_live_demo.py) | Full live demo against the AgentOS docker-compose stack |
286
+
287
+ ## Upstream pin
288
+
289
+ This release tracks **`claude-agent-sdk` 0.2.x**. The pinned upstream version is recorded in [`CHANGELOG.md`](CHANGELOG.md). Bump deliberately — wire-protocol field additions in upstream get re-exported automatically (identity-preserving), but any behavioral changes need a passthrough audit.
290
+
291
+ ## Development
292
+
293
+ ```bash
294
+ git clone https://github.com/open-gitagent/computer-agent-py
295
+ cd computer-agent-py
296
+ uv sync --all-extras --dev
297
+ uv run ruff check src tests
298
+ uv run ruff format --check src tests
299
+ uv run mypy src
300
+ uv run pytest -q # 120+ unit tests
301
+ uv run pytest -q -m integration # requires ANTHROPIC_API_KEY + claude CLI
302
+ uv build
303
+ ```
304
+
305
+ ## License
306
+
307
+ MIT — see [`LICENSE`](LICENSE).
@@ -0,0 +1,25 @@
1
+ computeragent/__init__.py,sha256=w_MWC-nCutEhDdnHx4SiKFToiVex8lHHKyIG1DHexzA,2939
2
+ computeragent/_proxy/__init__.py,sha256=7x5lm5tt4NhW8-4LvlM1grmdH0D_H5Ijc7a_BQjmMU8,218
3
+ computeragent/_proxy/client.py,sha256=FxRfA8mUdVTjQhlf4xTHCbUddZruvTPLzUOLWs5hzx8,8768
4
+ computeragent/_proxy/query.py,sha256=VIKMkbUPVHL3dUF4wggbXb0Gu35cTcu2HYd6-yL1nUg,6107
5
+ computeragent/policy/__init__.py,sha256=pKiV2ulBZW8khTa57UfqXGnicMviYxqL3CTO37Xo2js,1814
6
+ computeragent/policy/authorizer.py,sha256=PlkIcKtnSwl-B9f2FspX3-nVfzNLa43itV8EgnjW0BU,6310
7
+ computeragent/policy/cedar.py,sha256=mofBDW3HntADaim2WcMLUAXeZ9_OKaXuhMsaLRqzw10,6959
8
+ computeragent/policy/opa.py,sha256=Pho2_a97UhEpFsdvHJ71dXTnytqBYCmYb2GYpR7dABM,4920
9
+ computeragent/policy/types.py,sha256=sAGXyqT_-I-u9y2Mp583vkg86X9EOuI4YVBnaElnFVo,3488
10
+ computeragent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ computeragent/telemetry/__init__.py,sha256=VZFpiN6hJHu5w8ISROlrRvotKyGdGlbSWjmXdu6mTEk,647
12
+ computeragent/telemetry/config.py,sha256=luSsIftLniXQ0n9rDY3oFaA19I3D5tZHTL9wFS9iExk,6475
13
+ computeragent/telemetry/event.py,sha256=qsBvY3q4ORUSaUC49pzHWnH_27Qy9eujT4KCHMlimgM,13819
14
+ computeragent/telemetry/middleware/__init__.py,sha256=4poNMyaJuiVhE0KSvYcTwCbcwbNAqOUJb6HeSkn160M,222
15
+ computeragent/telemetry/middleware/guardrails.py,sha256=elcT3AdvOTddwY-Lp0rQKchTlCAq3Y2-ky3cp9n2lbo,5435
16
+ computeragent/telemetry/middleware/pii.py,sha256=xD2JRR97GNw_5TMz1PhYLcl2HkLuLS4BQrfhOjsJMVg,6453
17
+ computeragent/telemetry/pipeline.py,sha256=bxuYJ1oNzRyT992I6SUF-K65MJywKpFZBp6-zcc3HOI,5667
18
+ computeragent/telemetry/sinks/__init__.py,sha256=sDBRPu9-9pDWFeavxzs6mo3EXIL8jJyElB8hC03e4uc,1009
19
+ computeragent/telemetry/sinks/agentos.py,sha256=vOZq-kwzGFHCiaMj82AOQqKGxcDLePngwlos7SMhs4Y,17707
20
+ computeragent/telemetry/sinks/message_archive.py,sha256=N5kEjCHjph0oevY7n0EeRd2c0bFppSrlpHm3Ijcu4ao,5339
21
+ computeragent/telemetry/sinks/otel.py,sha256=xOUufyiJpZsuPCmUp0bi7onCcEUPmGE3xXfyj9le5Y8,15914
22
+ computeragent/types.py,sha256=ttjh1Ovb4zWWUv68jhCtb14lH83YsapP8ailmpo7PbY,475
23
+ computer_agent_py-0.1.0.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
24
+ computer_agent_py-0.1.0.dist-info/METADATA,sha256=WsCcBdn8DSTEreP49T1ir1c2T35nwprXYXyZYLkcfg8,16897
25
+ computer_agent_py-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,90 @@
1
+ """``computeragent`` — drop-in replacement for ``claude_agent_sdk``.
2
+
3
+ Every public symbol from ``claude_agent_sdk`` is re-exported here with identity
4
+ preserved (``computeragent.ResultMessage is claude_agent_sdk.ResultMessage``).
5
+ ``query`` and ``ClaudeSDKClient`` are wrapped so that every message flowing
6
+ through the upstream stream also flows through a telemetry pipeline with
7
+ configurable PII-redaction / guardrail middleware and pluggable sinks
8
+ (OpenTelemetry, AgentOS registry).
9
+
10
+ Drop-in usage::
11
+
12
+ # before
13
+ from claude_agent_sdk import ClaudeAgentOptions, query
14
+ from claude_agent_sdk.types import ResultMessage
15
+
16
+ # after — every other line stays identical
17
+ from computeragent import ClaudeAgentOptions, query
18
+ from computeragent.types import ResultMessage
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import claude_agent_sdk as _cas
24
+
25
+ # Re-export every public symbol from the upstream package by walking its
26
+ # `__all__`. This is identity-preserving — `computeragent.ResultMessage is
27
+ # claude_agent_sdk.ResultMessage` evaluates True — so user-side
28
+ # `isinstance(msg, ResultMessage)` keeps working regardless of import origin.
29
+ # Skip `__version__` so our own version (set below) wins.
30
+ _upstream_all = [n for n in getattr(_cas, "__all__", []) if n != "__version__"]
31
+ for _name in _upstream_all:
32
+ globals()[_name] = getattr(_cas, _name)
33
+
34
+ __version__ = "0.1.0"
35
+ upstream_version = getattr(_cas, "__version__", "unknown")
36
+
37
+ # Override the two entry points with the proxied versions. Same signatures,
38
+ # same yielded types — but each yielded message also flows through the
39
+ # telemetry pipeline.
40
+ from ._proxy.client import ClaudeSDKClient # noqa: E402
41
+ from ._proxy.query import query # noqa: E402
42
+
43
+ # Policy surface (v0.2 — tool-use authorization). Engines themselves
44
+ # (OpaPolicyEngine, CedarPolicyEngine) stay in ``computeragent.policy`` so
45
+ # users opt into Cedar's extra deliberately.
46
+ from .policy import ( # noqa: E402
47
+ PolicyAction,
48
+ PolicyDecision,
49
+ PolicyEngine,
50
+ PolicyInput,
51
+ PolicyPrincipal,
52
+ PolicyResource,
53
+ PolicyResult,
54
+ PolicyToolAuthorizer,
55
+ )
56
+
57
+ # Telemetry surface (additions beyond the drop-in mirror).
58
+ from .telemetry import ( # noqa: E402
59
+ GuardrailFilter,
60
+ PiiRedactor,
61
+ Pipeline,
62
+ TelemetryEvent,
63
+ TelemetryMiddleware,
64
+ TelemetrySink,
65
+ configure,
66
+ get_pipeline,
67
+ shutdown,
68
+ )
69
+
70
+ __all__ = sorted({*_upstream_all, "__version__"}) + [
71
+ # telemetry + policy add-ons (alphabetical at the tail so the public
72
+ # symbol list stays scannable when grepped against `claude_agent_sdk.__all__`)
73
+ "GuardrailFilter",
74
+ "PiiRedactor",
75
+ "Pipeline",
76
+ "PolicyAction",
77
+ "PolicyDecision",
78
+ "PolicyEngine",
79
+ "PolicyInput",
80
+ "PolicyPrincipal",
81
+ "PolicyResource",
82
+ "PolicyResult",
83
+ "PolicyToolAuthorizer",
84
+ "TelemetryEvent",
85
+ "TelemetryMiddleware",
86
+ "TelemetrySink",
87
+ "configure",
88
+ "get_pipeline",
89
+ "shutdown",
90
+ ]
@@ -0,0 +1,8 @@
1
+ """Proxied entry points — same signatures as upstream, plus telemetry tap."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .client import ClaudeSDKClient
6
+ from .query import query
7
+
8
+ __all__ = ["ClaudeSDKClient", "query"]
@@ -0,0 +1,225 @@
1
+ """``ClaudeSDKClient`` — wraps upstream's class with a telemetry tap.
2
+
3
+ The wrapper holds an instance of the upstream client and forwards every
4
+ public method. ``receive_messages`` and ``receive_response`` re-yield messages
5
+ through the telemetry pipeline so multi-turn callers get the same
6
+ observability as :func:`computeragent.query`.
7
+
8
+ Control methods (``set_model``, ``get_context_usage``, etc.) pass straight
9
+ through — no telemetry for those today (can be added later as
10
+ ``operation_started`` / ``operation_ended`` events).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import time
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ import claude_agent_sdk
20
+ from claude_agent_sdk.types import AssistantMessage, ResultMessage, TextBlock
21
+
22
+ from ..telemetry import TelemetryEvent, get_pipeline, new_session_id
23
+ from .query import _derive_agent_name
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import AsyncIterable, AsyncIterator
27
+
28
+ logger = logging.getLogger("computeragent")
29
+
30
+
31
+ class ClaudeSDKClient:
32
+ """Drop-in for :class:`claude_agent_sdk.ClaudeSDKClient`.
33
+
34
+ Same surface, same semantics, plus telemetry. The wrapper does not hold
35
+ extra state beyond the upstream instance and a per-session id used to
36
+ correlate emitted events.
37
+ """
38
+
39
+ def __init__(self, options: Any = None, transport: Any = None) -> None:
40
+ kwargs: dict[str, Any] = {}
41
+ if options is not None:
42
+ kwargs["options"] = options
43
+ if transport is not None:
44
+ kwargs["transport"] = transport
45
+ self._inner = claude_agent_sdk.ClaudeSDKClient(**kwargs)
46
+ self._options = options
47
+ self._session_id: str = new_session_id()
48
+ # Upstream's own session id is captured for join-by-id but is NOT
49
+ # used as the sink/event session_id (see _proxy.query for the
50
+ # placeholder-vs-upstream rationale).
51
+ self._upstream_session_id: str | None = None
52
+ self._agent_name = _derive_agent_name(options)
53
+ self._started_at: float | None = None
54
+ self._last_result: ResultMessage | None = None
55
+ self._last_assistant_text: str = ""
56
+ self._connected = False
57
+
58
+ # ── lifecycle ----------------------------------------------------------
59
+
60
+ async def __aenter__(self) -> ClaudeSDKClient:
61
+ await self.connect()
62
+ return self
63
+
64
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
65
+ try:
66
+ await self.disconnect()
67
+ except Exception: # noqa: BLE001
68
+ logger.debug("disconnect in __aexit__ failed", exc_info=True)
69
+ return False
70
+
71
+ async def connect(self, prompt: str | AsyncIterable[dict[str, Any]] | None = None) -> None:
72
+ pipeline = get_pipeline()
73
+ self._started_at = time.monotonic()
74
+ await pipeline.emit(
75
+ TelemetryEvent.session_started(
76
+ session_id=self._session_id,
77
+ agent_name=self._agent_name,
78
+ options=self._options,
79
+ prompt=prompt if prompt is not None else "<connect>",
80
+ )
81
+ )
82
+ if prompt is None:
83
+ await self._inner.connect()
84
+ else:
85
+ await self._inner.connect(prompt)
86
+ self._connected = True
87
+
88
+ async def disconnect(self) -> None:
89
+ if not self._connected:
90
+ return
91
+ try:
92
+ await self._emit_session_ended()
93
+ finally:
94
+ self._connected = False
95
+ try:
96
+ await self._inner.disconnect()
97
+ finally:
98
+ try:
99
+ await get_pipeline().flush()
100
+ except Exception: # noqa: BLE001
101
+ logger.debug("flush in disconnect failed", exc_info=True)
102
+
103
+ # ── streaming ----------------------------------------------------------
104
+
105
+ async def query(
106
+ self, prompt: str | AsyncIterable[dict[str, Any]], session_id: str = "default"
107
+ ) -> None:
108
+ await self._inner.query(prompt, session_id=session_id)
109
+
110
+ async def receive_messages(self) -> AsyncIterator[Any]:
111
+ pipeline = get_pipeline()
112
+ async for msg in self._inner.receive_messages():
113
+ real_sid = getattr(msg, "session_id", None)
114
+ if (
115
+ isinstance(real_sid, str)
116
+ and real_sid
117
+ and real_sid != self._session_id
118
+ and self._upstream_session_id is None
119
+ ):
120
+ # Capture but don't rebind — see _proxy.query for rationale.
121
+ self._upstream_session_id = real_sid
122
+ for ev in TelemetryEvent.from_message(
123
+ msg, session_id=self._session_id, agent_name=self._agent_name
124
+ ):
125
+ await pipeline.emit(ev)
126
+ if isinstance(msg, AssistantMessage):
127
+ for block in getattr(msg, "content", []) or []:
128
+ if isinstance(block, TextBlock):
129
+ self._last_assistant_text = block.text
130
+ if isinstance(msg, ResultMessage):
131
+ self._last_result = msg
132
+ yield msg
133
+
134
+ async def receive_response(self) -> AsyncIterator[Any]:
135
+ pipeline = get_pipeline()
136
+ async for msg in self._inner.receive_response():
137
+ real_sid = getattr(msg, "session_id", None)
138
+ if (
139
+ isinstance(real_sid, str)
140
+ and real_sid
141
+ and real_sid != self._session_id
142
+ and self._upstream_session_id is None
143
+ ):
144
+ self._upstream_session_id = real_sid
145
+ for ev in TelemetryEvent.from_message(
146
+ msg, session_id=self._session_id, agent_name=self._agent_name
147
+ ):
148
+ await pipeline.emit(ev)
149
+ if isinstance(msg, AssistantMessage):
150
+ for block in getattr(msg, "content", []) or []:
151
+ if isinstance(block, TextBlock):
152
+ self._last_assistant_text = block.text
153
+ if isinstance(msg, ResultMessage):
154
+ self._last_result = msg
155
+ yield msg
156
+
157
+ async def interrupt(self) -> None:
158
+ await self._inner.interrupt()
159
+
160
+ # ── pass-through control methods ---------------------------------------
161
+
162
+ async def set_model(self, model: str | None = None) -> None:
163
+ await self._inner.set_model(model)
164
+
165
+ async def set_permission_mode(self, mode: Any) -> None:
166
+ await self._inner.set_permission_mode(mode)
167
+
168
+ async def get_context_usage(self) -> Any:
169
+ return await self._inner.get_context_usage()
170
+
171
+ async def get_mcp_status(self) -> Any:
172
+ return await self._inner.get_mcp_status()
173
+
174
+ async def get_server_info(self) -> dict[str, Any] | None:
175
+ # Upstream's `get_server_info` is async; the proxy mirrors that.
176
+ result: dict[str, Any] | None = await self._inner.get_server_info()
177
+ return result
178
+
179
+ async def reconnect_mcp_server(self, server_name: str) -> None:
180
+ await self._inner.reconnect_mcp_server(server_name)
181
+
182
+ async def toggle_mcp_server(self, server_name: str, enabled: bool) -> None:
183
+ await self._inner.toggle_mcp_server(server_name, enabled)
184
+
185
+ async def stop_task(self, task_id: str) -> None:
186
+ await self._inner.stop_task(task_id)
187
+
188
+ async def rewind_files(self, user_message_id: str) -> None:
189
+ await self._inner.rewind_files(user_message_id)
190
+
191
+ # ── internals ----------------------------------------------------------
192
+
193
+ async def _emit_session_ended(self) -> None:
194
+ if self._started_at is None:
195
+ return
196
+ duration_ms = (time.monotonic() - self._started_at) * 1000.0
197
+ pipeline = get_pipeline()
198
+ if self._last_result is not None:
199
+ ev = TelemetryEvent.session_ended_ok(
200
+ session_id=self._session_id,
201
+ agent_name=self._agent_name,
202
+ result=self._last_result,
203
+ duration_ms=duration_ms,
204
+ last_assistant_text=self._last_assistant_text,
205
+ )
206
+ if self._upstream_session_id is not None:
207
+ ev.payload["upstream_session_id"] = self._upstream_session_id
208
+ await pipeline.emit(ev)
209
+ else:
210
+ await pipeline.emit(
211
+ TelemetryEvent(
212
+ kind="session_ended",
213
+ session_id=self._session_id,
214
+ agent_name=self._agent_name,
215
+ payload={
216
+ "is_error": False,
217
+ "subtype": "no_result",
218
+ "result": self._last_assistant_text,
219
+ "duration_ms": duration_ms,
220
+ "num_turns": 0,
221
+ "total_cost_usd": 0.0,
222
+ "usage": {},
223
+ },
224
+ )
225
+ )