ams-observability 0.1.0__tar.gz
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.
- ams_observability-0.1.0/.github/workflows/release.yml +26 -0
- ams_observability-0.1.0/.gitignore +13 -0
- ams_observability-0.1.0/LICENSE +21 -0
- ams_observability-0.1.0/PKG-INFO +180 -0
- ams_observability-0.1.0/README.md +156 -0
- ams_observability-0.1.0/ams/__init__.py +38 -0
- ams_observability-0.1.0/ams/claude.py +46 -0
- ams_observability-0.1.0/ams/pricing.py +45 -0
- ams_observability-0.1.0/ams/redact.py +31 -0
- ams_observability-0.1.0/ams/schema.py +168 -0
- ams_observability-0.1.0/ams/storage/__init__.py +30 -0
- ams_observability-0.1.0/ams/storage/local.py +35 -0
- ams_observability-0.1.0/ams/storage/s3.py +78 -0
- ams_observability-0.1.0/ams/tracer.py +399 -0
- ams_observability-0.1.0/conftest.py +0 -0
- ams_observability-0.1.0/docs/architecture.md +92 -0
- ams_observability-0.1.0/docs/frontend-notes.md +24 -0
- ams_observability-0.1.0/docs/schema.md +99 -0
- ams_observability-0.1.0/examples/basic_query.py +30 -0
- ams_observability-0.1.0/examples/demos/README.md +93 -0
- ams_observability-0.1.0/examples/demos/research_agent.py +72 -0
- ams_observability-0.1.0/examples/demos/simple_agent.py +40 -0
- ams_observability-0.1.0/examples/demos/view_session.py +60 -0
- ams_observability-0.1.0/examples/with_client.py +30 -0
- ams_observability-0.1.0/pyproject.toml +44 -0
- ams_observability-0.1.0/tests/__init__.py +0 -0
- ams_observability-0.1.0/tests/fakes.py +55 -0
- ams_observability-0.1.0/tests/test_storage.py +56 -0
- ams_observability-0.1.0/tests/test_tracer.py +235 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes ams-observability to PyPI when a GitHub Release is published.
|
|
4
|
+
# Uses PyPI Trusted Publishing (OIDC) — no API token is stored anywhere.
|
|
5
|
+
# One-time setup on PyPI: add a trusted publisher for project
|
|
6
|
+
# `ams-observability` (owner `mathu97`, repo `ams`, workflow `release.yml`).
|
|
7
|
+
#
|
|
8
|
+
# To cut a release: bump `version` in pyproject.toml, then create a GitHub
|
|
9
|
+
# Release (tag like v0.1.0). Publishing runs automatically.
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
publish:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
permissions:
|
|
19
|
+
id-token: write # required for trusted publishing
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: astral-sh/setup-uv@v5
|
|
23
|
+
- name: Build sdist and wheel
|
|
24
|
+
run: uv build
|
|
25
|
+
- name: Publish to PyPI
|
|
26
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mathu97
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ams-observability
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A super simple monitoring system for Claude agents — full session traces as JSON in blob storage.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mathu97/ams
|
|
6
|
+
Project-URL: Repository, https://github.com/mathu97/ams
|
|
7
|
+
Author: mathu97
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,claude,llm,monitoring,observability,tracing
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: System :: Monitoring
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: boto3>=1.28
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# AMS — Agent Monitoring System
|
|
26
|
+
|
|
27
|
+
A **super simple** monitoring system for [Claude agents](https://docs.claude.com/en/api/agent-sdk/overview). Capture a whole Claude Agent SDK session end to end — every tool call, every subagent and *why* it was invoked, the model's reasoning, results, timing, and cost — as **one readable JSON object** in blob storage.
|
|
28
|
+
|
|
29
|
+
No collector, no database, no agent. One JSON file per session in S3-compatible storage. Built to be trivially easy to read and filter (the things that make Arize and friends painful).
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from ams.claude import traced_query
|
|
33
|
+
|
|
34
|
+
async for message in traced_query(prompt="Cancel my membership", options=options):
|
|
35
|
+
...
|
|
36
|
+
# session written to storage automatically when the stream ends
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That's the whole integration. Swap `query` for `traced_query`.
|
|
40
|
+
|
|
41
|
+
## Why
|
|
42
|
+
|
|
43
|
+
We monitor our Claude agents with Arize today, but it's hard to read, and hard to search/filter for a single session. AMS keeps the data model deliberately flat and typed so a session is obvious to a human and easy to query by a machine. Field names follow the [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) (`gen_ai.*`) where there's a natural equivalent, so the data can later be re-emitted as OTLP without renaming.
|
|
44
|
+
|
|
45
|
+
## What it captures
|
|
46
|
+
|
|
47
|
+
A session is one trace of ordered **events**:
|
|
48
|
+
|
|
49
|
+
| Event | Source | Detail captured |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `user_prompt` | `UserPromptSubmit` hook | the prompt |
|
|
52
|
+
| `llm_message` | message stream | model, **thinking / chain-of-thought**, assistant text, token usage |
|
|
53
|
+
| `tool_call` | `PreToolUse` + `PostToolUse` / `PostToolUseFailure` hooks | tool name, input, result, error, **timing** |
|
|
54
|
+
| `subagent` | `SubagentStart` / `SubagentStop` hooks | agent type, **why it was invoked** (the prompt), transcript path; child tool calls nest underneath |
|
|
55
|
+
| `notification` | `Notification` hook | message |
|
|
56
|
+
|
|
57
|
+
Plus session **totals**: token usage (incl. cache read/write), cost (USD), turn count, tool/subagent/error counts, and wall-clock + API duration.
|
|
58
|
+
|
|
59
|
+
The Claude Agent SDK has **no built-in OpenTelemetry** — AMS captures everything through hooks and the message stream, which together are the only place this data lives.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install ams-observability # published name; you import it as `ams`
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or from a local checkout: `pip install -e .`. S3-compatible storage (boto3) is included by default.
|
|
68
|
+
|
|
69
|
+
Requires Python 3.10+ and the [`claude-agent-sdk`](https://pypi.org/project/claude-agent-sdk/) in your project.
|
|
70
|
+
|
|
71
|
+
## Configure storage
|
|
72
|
+
|
|
73
|
+
S3-compatible storage is the default. Works against **AWS S3, Cloudflare R2, MinIO** — anything speaking the S3 API.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
export AMS_S3_BUCKET=my-agent-traces
|
|
77
|
+
export AMS_S3_PREFIX=ams # optional, default "ams"
|
|
78
|
+
export AMS_S3_ENDPOINT_URL=https://<account>.r2.cloudflarestorage.com # omit for AWS S3
|
|
79
|
+
export AMS_S3_REGION=auto
|
|
80
|
+
# credentials via the standard AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or write to local disk for development:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export AMS_STORAGE=local
|
|
87
|
+
export AMS_LOCAL_DIR=./ams-data # optional
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Layout in the bucket
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
{prefix}/sessions/{YYYY}/{MM}/{DD}/{session_id}.json full session
|
|
94
|
+
{prefix}/index/{session_id}.json compact summary (for listing/filtering)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The small `index/` objects let a frontend build a searchable session list without opening every full session.
|
|
98
|
+
|
|
99
|
+
## Usage
|
|
100
|
+
|
|
101
|
+
### One-call (drop-in for `query`)
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from ams import Agent
|
|
105
|
+
from ams.claude import traced_query
|
|
106
|
+
|
|
107
|
+
async for message in traced_query(
|
|
108
|
+
prompt="...",
|
|
109
|
+
options=options, # your ClaudeAgentOptions
|
|
110
|
+
agent=Agent(name="support-bot", version="2026.06"),
|
|
111
|
+
environment="prod",
|
|
112
|
+
tags=["voice", "cancellation"],
|
|
113
|
+
metadata={"team_id": "t_42"},
|
|
114
|
+
):
|
|
115
|
+
print(message)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### With `ClaudeSDKClient`
|
|
119
|
+
|
|
120
|
+
Merge AMS hooks into your options, feed messages to the tracer, and `finish()` when done:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from ams import Tracer
|
|
124
|
+
from ams.claude import instrument_options
|
|
125
|
+
|
|
126
|
+
tracer = Tracer(environment="prod", tags=["chat"])
|
|
127
|
+
options = instrument_options(my_options, tracer)
|
|
128
|
+
|
|
129
|
+
async with ClaudeSDKClient(options=options) as client:
|
|
130
|
+
await client.query("...")
|
|
131
|
+
async for message in client.receive_response():
|
|
132
|
+
tracer.record_message(message)
|
|
133
|
+
|
|
134
|
+
session = tracer.finish()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Custom storage
|
|
138
|
+
|
|
139
|
+
Pass any object with `put_session(session) -> str`:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
tracer = Tracer(storage=MyStorage())
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Options
|
|
146
|
+
|
|
147
|
+
| Tracer arg / env | Default | Notes |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `storage` / `AMS_STORAGE` | S3 | `local` to write to disk |
|
|
150
|
+
| `agent` | — | `Agent(name=..., version=...)` |
|
|
151
|
+
| `environment` | — | e.g. `prod`, `staging` |
|
|
152
|
+
| `tags`, `metadata` | — | free-form, promoted into the index for filtering |
|
|
153
|
+
| `capture_thinking` | `True` | record the model's reasoning blocks |
|
|
154
|
+
| `redact` / `AMS_REDACT` | `False` | opt-in PII redaction (email / phone / card / SSN) |
|
|
155
|
+
|
|
156
|
+
AMS never throws into your agent: hook and storage failures are logged, not raised.
|
|
157
|
+
|
|
158
|
+
## How it works
|
|
159
|
+
|
|
160
|
+
See [`docs/architecture.md`](docs/architecture.md) for the module map and the two-channel design (hooks + message stream) that AMS fuses into one session.
|
|
161
|
+
|
|
162
|
+
## Schema
|
|
163
|
+
|
|
164
|
+
See [`docs/schema.md`](docs/schema.md) for the full session JSON schema with an example. The contract lives in one file: [`ams/schema.py`](ams/schema.py).
|
|
165
|
+
|
|
166
|
+
## Frontend
|
|
167
|
+
|
|
168
|
+
A simple frontend to browse and filter sessions is planned (not built yet). See [`docs/frontend-notes.md`](docs/frontend-notes.md) for the intended design — it reads the `index/` summaries to list sessions and fetches a full session JSON on click.
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
python -m venv .venv && . .venv/bin/activate
|
|
174
|
+
pip install -e ".[dev]"
|
|
175
|
+
pytest
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# AMS — Agent Monitoring System
|
|
2
|
+
|
|
3
|
+
A **super simple** monitoring system for [Claude agents](https://docs.claude.com/en/api/agent-sdk/overview). Capture a whole Claude Agent SDK session end to end — every tool call, every subagent and *why* it was invoked, the model's reasoning, results, timing, and cost — as **one readable JSON object** in blob storage.
|
|
4
|
+
|
|
5
|
+
No collector, no database, no agent. One JSON file per session in S3-compatible storage. Built to be trivially easy to read and filter (the things that make Arize and friends painful).
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from ams.claude import traced_query
|
|
9
|
+
|
|
10
|
+
async for message in traced_query(prompt="Cancel my membership", options=options):
|
|
11
|
+
...
|
|
12
|
+
# session written to storage automatically when the stream ends
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's the whole integration. Swap `query` for `traced_query`.
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
We monitor our Claude agents with Arize today, but it's hard to read, and hard to search/filter for a single session. AMS keeps the data model deliberately flat and typed so a session is obvious to a human and easy to query by a machine. Field names follow the [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) (`gen_ai.*`) where there's a natural equivalent, so the data can later be re-emitted as OTLP without renaming.
|
|
20
|
+
|
|
21
|
+
## What it captures
|
|
22
|
+
|
|
23
|
+
A session is one trace of ordered **events**:
|
|
24
|
+
|
|
25
|
+
| Event | Source | Detail captured |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `user_prompt` | `UserPromptSubmit` hook | the prompt |
|
|
28
|
+
| `llm_message` | message stream | model, **thinking / chain-of-thought**, assistant text, token usage |
|
|
29
|
+
| `tool_call` | `PreToolUse` + `PostToolUse` / `PostToolUseFailure` hooks | tool name, input, result, error, **timing** |
|
|
30
|
+
| `subagent` | `SubagentStart` / `SubagentStop` hooks | agent type, **why it was invoked** (the prompt), transcript path; child tool calls nest underneath |
|
|
31
|
+
| `notification` | `Notification` hook | message |
|
|
32
|
+
|
|
33
|
+
Plus session **totals**: token usage (incl. cache read/write), cost (USD), turn count, tool/subagent/error counts, and wall-clock + API duration.
|
|
34
|
+
|
|
35
|
+
The Claude Agent SDK has **no built-in OpenTelemetry** — AMS captures everything through hooks and the message stream, which together are the only place this data lives.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install ams-observability # published name; you import it as `ams`
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or from a local checkout: `pip install -e .`. S3-compatible storage (boto3) is included by default.
|
|
44
|
+
|
|
45
|
+
Requires Python 3.10+ and the [`claude-agent-sdk`](https://pypi.org/project/claude-agent-sdk/) in your project.
|
|
46
|
+
|
|
47
|
+
## Configure storage
|
|
48
|
+
|
|
49
|
+
S3-compatible storage is the default. Works against **AWS S3, Cloudflare R2, MinIO** — anything speaking the S3 API.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export AMS_S3_BUCKET=my-agent-traces
|
|
53
|
+
export AMS_S3_PREFIX=ams # optional, default "ams"
|
|
54
|
+
export AMS_S3_ENDPOINT_URL=https://<account>.r2.cloudflarestorage.com # omit for AWS S3
|
|
55
|
+
export AMS_S3_REGION=auto
|
|
56
|
+
# credentials via the standard AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or write to local disk for development:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
export AMS_STORAGE=local
|
|
63
|
+
export AMS_LOCAL_DIR=./ams-data # optional
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Layout in the bucket
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
{prefix}/sessions/{YYYY}/{MM}/{DD}/{session_id}.json full session
|
|
70
|
+
{prefix}/index/{session_id}.json compact summary (for listing/filtering)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The small `index/` objects let a frontend build a searchable session list without opening every full session.
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
### One-call (drop-in for `query`)
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from ams import Agent
|
|
81
|
+
from ams.claude import traced_query
|
|
82
|
+
|
|
83
|
+
async for message in traced_query(
|
|
84
|
+
prompt="...",
|
|
85
|
+
options=options, # your ClaudeAgentOptions
|
|
86
|
+
agent=Agent(name="support-bot", version="2026.06"),
|
|
87
|
+
environment="prod",
|
|
88
|
+
tags=["voice", "cancellation"],
|
|
89
|
+
metadata={"team_id": "t_42"},
|
|
90
|
+
):
|
|
91
|
+
print(message)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### With `ClaudeSDKClient`
|
|
95
|
+
|
|
96
|
+
Merge AMS hooks into your options, feed messages to the tracer, and `finish()` when done:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from ams import Tracer
|
|
100
|
+
from ams.claude import instrument_options
|
|
101
|
+
|
|
102
|
+
tracer = Tracer(environment="prod", tags=["chat"])
|
|
103
|
+
options = instrument_options(my_options, tracer)
|
|
104
|
+
|
|
105
|
+
async with ClaudeSDKClient(options=options) as client:
|
|
106
|
+
await client.query("...")
|
|
107
|
+
async for message in client.receive_response():
|
|
108
|
+
tracer.record_message(message)
|
|
109
|
+
|
|
110
|
+
session = tracer.finish()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Custom storage
|
|
114
|
+
|
|
115
|
+
Pass any object with `put_session(session) -> str`:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
tracer = Tracer(storage=MyStorage())
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Options
|
|
122
|
+
|
|
123
|
+
| Tracer arg / env | Default | Notes |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `storage` / `AMS_STORAGE` | S3 | `local` to write to disk |
|
|
126
|
+
| `agent` | — | `Agent(name=..., version=...)` |
|
|
127
|
+
| `environment` | — | e.g. `prod`, `staging` |
|
|
128
|
+
| `tags`, `metadata` | — | free-form, promoted into the index for filtering |
|
|
129
|
+
| `capture_thinking` | `True` | record the model's reasoning blocks |
|
|
130
|
+
| `redact` / `AMS_REDACT` | `False` | opt-in PII redaction (email / phone / card / SSN) |
|
|
131
|
+
|
|
132
|
+
AMS never throws into your agent: hook and storage failures are logged, not raised.
|
|
133
|
+
|
|
134
|
+
## How it works
|
|
135
|
+
|
|
136
|
+
See [`docs/architecture.md`](docs/architecture.md) for the module map and the two-channel design (hooks + message stream) that AMS fuses into one session.
|
|
137
|
+
|
|
138
|
+
## Schema
|
|
139
|
+
|
|
140
|
+
See [`docs/schema.md`](docs/schema.md) for the full session JSON schema with an example. The contract lives in one file: [`ams/schema.py`](ams/schema.py).
|
|
141
|
+
|
|
142
|
+
## Frontend
|
|
143
|
+
|
|
144
|
+
A simple frontend to browse and filter sessions is planned (not built yet). See [`docs/frontend-notes.md`](docs/frontend-notes.md) for the intended design — it reads the `index/` summaries to list sessions and fetches a full session JSON on click.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
python -m venv .venv && . .venv/bin/activate
|
|
150
|
+
pip install -e ".[dev]"
|
|
151
|
+
pytest
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""AMS — a super simple monitoring system for Claude agents.
|
|
2
|
+
|
|
3
|
+
Capture a whole Claude Agent SDK session end to end — every tool call, every
|
|
4
|
+
subagent and why it was invoked, the model's reasoning, results, timing and
|
|
5
|
+
cost — as one readable JSON object in blob storage.
|
|
6
|
+
|
|
7
|
+
from ams.claude import traced_query
|
|
8
|
+
|
|
9
|
+
async for message in traced_query(prompt="...", options=options):
|
|
10
|
+
...
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .schema import (
|
|
14
|
+
SCHEMA_VERSION,
|
|
15
|
+
Agent,
|
|
16
|
+
Event,
|
|
17
|
+
EventType,
|
|
18
|
+
Session,
|
|
19
|
+
Status,
|
|
20
|
+
Totals,
|
|
21
|
+
Usage,
|
|
22
|
+
)
|
|
23
|
+
from .tracer import Tracer
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Tracer",
|
|
29
|
+
"Session",
|
|
30
|
+
"Event",
|
|
31
|
+
"EventType",
|
|
32
|
+
"Status",
|
|
33
|
+
"Totals",
|
|
34
|
+
"Usage",
|
|
35
|
+
"Agent",
|
|
36
|
+
"SCHEMA_VERSION",
|
|
37
|
+
"__version__",
|
|
38
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Glue between AMS and `claude_agent_sdk`. This is the whole integration
|
|
2
|
+
surface: swap `query` for `traced_query`, or merge `tracer.hooks()` into your
|
|
3
|
+
options if you drive a ClaudeSDKClient yourself."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, AsyncIterator, Optional
|
|
8
|
+
|
|
9
|
+
from .tracer import Tracer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def instrument_options(options: Any, tracer: Tracer) -> Any:
|
|
13
|
+
"""Merge AMS hooks into an existing ClaudeAgentOptions, keeping any of yours."""
|
|
14
|
+
merged = dict(getattr(options, "hooks", None) or {})
|
|
15
|
+
for name, matchers in tracer.hooks().items():
|
|
16
|
+
merged[name] = list(merged.get(name, [])) + list(matchers)
|
|
17
|
+
options.hooks = merged
|
|
18
|
+
return options
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def traced_query(
|
|
22
|
+
*,
|
|
23
|
+
prompt: Any,
|
|
24
|
+
options: Any = None,
|
|
25
|
+
tracer: Optional[Tracer] = None,
|
|
26
|
+
**tracer_kwargs: Any,
|
|
27
|
+
) -> AsyncIterator[Any]:
|
|
28
|
+
"""Drop-in replacement for `claude_agent_sdk.query` that records the session.
|
|
29
|
+
|
|
30
|
+
from ams.claude import traced_query
|
|
31
|
+
|
|
32
|
+
async for message in traced_query(prompt="...", options=options):
|
|
33
|
+
print(message)
|
|
34
|
+
|
|
35
|
+
Extra keyword args (storage, agent, environment, tags, metadata, redact)
|
|
36
|
+
are forwarded to `Tracer`. On stream completion the session is written to
|
|
37
|
+
storage automatically.
|
|
38
|
+
"""
|
|
39
|
+
from claude_agent_sdk import ClaudeAgentOptions, query
|
|
40
|
+
|
|
41
|
+
tracer = tracer or Tracer(**tracer_kwargs)
|
|
42
|
+
options = options or ClaudeAgentOptions()
|
|
43
|
+
options = instrument_options(options, tracer)
|
|
44
|
+
|
|
45
|
+
async for message in tracer.watch(query(prompt=prompt, options=options)):
|
|
46
|
+
yield message
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Token -> USD cost. The Claude Agent SDK already reports `total_cost_usd` on
|
|
2
|
+
the result message, so AMS uses that for the session total. This table is a
|
|
3
|
+
fallback for costing an individual LLM call from its token usage.
|
|
4
|
+
|
|
5
|
+
Prices are USD per million tokens. Update as needed; matching is by substring so
|
|
6
|
+
dated model ids (e.g. `claude-opus-4-8-20260101`) resolve to the right family.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .schema import Usage
|
|
14
|
+
|
|
15
|
+
# (input, output, cache_write, cache_read) per million tokens
|
|
16
|
+
_PRICES: dict[str, tuple[float, float, float, float]] = {
|
|
17
|
+
"claude-opus-4": (15.0, 75.0, 18.75, 1.5),
|
|
18
|
+
"claude-sonnet-4": (3.0, 15.0, 3.75, 0.3),
|
|
19
|
+
"claude-haiku-4": (1.0, 5.0, 1.25, 0.1),
|
|
20
|
+
"claude-3-5-haiku": (0.8, 4.0, 1.0, 0.08),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _match(model: str) -> Optional[tuple[float, float, float, float]]:
|
|
25
|
+
model = model.lower()
|
|
26
|
+
for key, price in _PRICES.items():
|
|
27
|
+
if key in model:
|
|
28
|
+
return price
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cost_usd(model: Optional[str], usage: Optional[Usage]) -> Optional[float]:
|
|
33
|
+
if not model or usage is None:
|
|
34
|
+
return None
|
|
35
|
+
price = _match(model)
|
|
36
|
+
if price is None:
|
|
37
|
+
return None
|
|
38
|
+
p_in, p_out, p_cw, p_cr = price
|
|
39
|
+
total = (
|
|
40
|
+
usage.input_tokens * p_in
|
|
41
|
+
+ usage.output_tokens * p_out
|
|
42
|
+
+ usage.cache_creation_input_tokens * p_cw
|
|
43
|
+
+ usage.cache_read_input_tokens * p_cr
|
|
44
|
+
)
|
|
45
|
+
return round(total / 1_000_000, 6)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Optional PII redaction. Off by default — AMS captures full detail unless you
|
|
2
|
+
opt in. Turn it on with `Tracer(redact=True)` or `AMS_REDACT=1` when sessions
|
|
3
|
+
may contain sensitive caller data (phone numbers, emails, cards)."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
_PATTERNS = [
|
|
11
|
+
(re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"), "[email]"),
|
|
12
|
+
(re.compile(r"\+?\d[\d\s().-]{7,}\d"), "[phone]"),
|
|
13
|
+
(re.compile(r"\b(?:\d[ -]*?){13,16}\b"), "[card]"),
|
|
14
|
+
(re.compile(r"\b\d{3}-\d{2}-\d{4}\b"), "[ssn]"),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def redact_text(text: str) -> str:
|
|
19
|
+
for pattern, replacement in _PATTERNS:
|
|
20
|
+
text = pattern.sub(replacement, text)
|
|
21
|
+
return text
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def redact(value: Any) -> Any:
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
return redact_text(value)
|
|
27
|
+
if isinstance(value, dict):
|
|
28
|
+
return {k: redact(v) for k, v in value.items()}
|
|
29
|
+
if isinstance(value, list):
|
|
30
|
+
return [redact(v) for v in value]
|
|
31
|
+
return value
|