chatsee-claude-agent-sdk 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.
- chatsee_claude_agent_sdk-0.1.0/PKG-INFO +112 -0
- chatsee_claude_agent_sdk-0.1.0/README.md +97 -0
- chatsee_claude_agent_sdk-0.1.0/chatsee_claude_agent_sdk/__init__.py +232 -0
- chatsee_claude_agent_sdk-0.1.0/chatsee_claude_agent_sdk/_capture.py +338 -0
- chatsee_claude_agent_sdk-0.1.0/chatsee_claude_agent_sdk.egg-info/PKG-INFO +112 -0
- chatsee_claude_agent_sdk-0.1.0/chatsee_claude_agent_sdk.egg-info/SOURCES.txt +10 -0
- chatsee_claude_agent_sdk-0.1.0/chatsee_claude_agent_sdk.egg-info/dependency_links.txt +1 -0
- chatsee_claude_agent_sdk-0.1.0/chatsee_claude_agent_sdk.egg-info/requires.txt +7 -0
- chatsee_claude_agent_sdk-0.1.0/chatsee_claude_agent_sdk.egg-info/top_level.txt +1 -0
- chatsee_claude_agent_sdk-0.1.0/pyproject.toml +30 -0
- chatsee_claude_agent_sdk-0.1.0/setup.cfg +4 -0
- chatsee_claude_agent_sdk-0.1.0/tests/test_claude_agent_sdk_contract.py +464 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chatsee-claude-agent-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ChatSee connector for the Claude Agent SDK — capture every turn with one line.
|
|
5
|
+
Maintainer-email: ChatSee <contact@chatsee.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: chatsee-ai>=0.14.0
|
|
10
|
+
Requires-Dist: claude-agent-sdk>=0.2.100
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
14
|
+
Requires-Dist: chatsee-connector-testing; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# chatsee-claude-agent-sdk
|
|
17
|
+
|
|
18
|
+
ChatSee connector for the **Claude Agent SDK** — Anthropic's agent framework,
|
|
19
|
+
the one that powers Claude Code.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install chatsee-claude-agent-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from chatsee_claude_agent_sdk import instrument_claude_agent_sdk
|
|
27
|
+
|
|
28
|
+
chatsee = instrument_claude_agent_sdk(agent_id="...", tenant_id="...")
|
|
29
|
+
|
|
30
|
+
async for message in chatsee.query(prompt="where is my order", options=opts):
|
|
31
|
+
...
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`chatsee-ai` is installed for you as a dependency.
|
|
35
|
+
|
|
36
|
+
## Not the same thing as `chatsee-anthropic`
|
|
37
|
+
|
|
38
|
+
| | `chatsee-anthropic` | this |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| Package it hooks | `anthropic` | `claude-agent-sdk` |
|
|
41
|
+
| Who writes the agent loop | the customer | Anthropic |
|
|
42
|
+
| What we hook | `client.messages.create` | the `query()` message stream |
|
|
43
|
+
|
|
44
|
+
Use `chatsee-anthropic` when they call the Messages API and run their own
|
|
45
|
+
tool-use loop. Use this when they call `query()` and let the SDK run it.
|
|
46
|
+
|
|
47
|
+
## Why a handle rather than a one-liner
|
|
48
|
+
|
|
49
|
+
There is no client object to patch — the entry point is a module-level
|
|
50
|
+
`query()`. So `instrument_claude_agent_sdk` returns a handle whose `.query` is
|
|
51
|
+
a drop-in: every message still reaches the caller, unchanged and in order,
|
|
52
|
+
while the turn is assembled alongside.
|
|
53
|
+
|
|
54
|
+
`claude_agent_sdk.query` is replaced too, so code calling it **through the
|
|
55
|
+
module** needs no change. Code that did `from claude_agent_sdk import query`
|
|
56
|
+
already holds its own reference to the original and cannot be reached that way
|
|
57
|
+
— that code uses the handle.
|
|
58
|
+
|
|
59
|
+
## What it captures
|
|
60
|
+
|
|
61
|
+
Per turn: the prompt, the final answer, the system prompt, every tool call with
|
|
62
|
+
its arguments and result, and **one model call per model**, with tokens, cached
|
|
63
|
+
tokens and a real dollar cost.
|
|
64
|
+
|
|
65
|
+
This SDK is the only one that states its own cost (`total_cost_usd`), so that
|
|
66
|
+
figure is passed through rather than estimated. Nothing here invents a price,
|
|
67
|
+
and a turn's cost is never divided across models — a split we made up would
|
|
68
|
+
look like measurement.
|
|
69
|
+
|
|
70
|
+
## Three things this framework does that nothing else does
|
|
71
|
+
|
|
72
|
+
**The CLI's system prompt is enormous.** Claude Code injects roughly 21k cached
|
|
73
|
+
tokens on every call, so a one-line "hello" costs around $0.027. Those land in
|
|
74
|
+
`cache_read_input_tokens`, not `input_tokens` — both are captured, or the turn
|
|
75
|
+
looks far cheaper than the bill.
|
|
76
|
+
|
|
77
|
+
**The CLI calls its own tools.** It loads tools on demand and emits a
|
|
78
|
+
`ToolSearch` tool-use alongside the agent's real one. Those are filtered: a
|
|
79
|
+
support agent that never touched a search tool must not show one in its trace.
|
|
80
|
+
|
|
81
|
+
**Tool names arrive MCP-qualified.** Custom tools reach the model as
|
|
82
|
+
`mcp__<server>__<tool>`. The customer named it `get_order_status`, so the
|
|
83
|
+
prefix is stripped and that is what the screens say.
|
|
84
|
+
|
|
85
|
+
## Guarantees
|
|
86
|
+
|
|
87
|
+
* **Fails open.** A failure inside capture is logged and swallowed.
|
|
88
|
+
* **Never consumes the stream.** Every message is forwarded unchanged, in order.
|
|
89
|
+
* **Idempotent.** Instrumenting twice does not capture twice.
|
|
90
|
+
* **Concurrency-safe.** State is per `query()` call, not per handle.
|
|
91
|
+
|
|
92
|
+
All four are enforced by the contract suite, not just intended.
|
|
93
|
+
|
|
94
|
+
## Tests
|
|
95
|
+
|
|
96
|
+
No network, no API key, no CLI subprocess — the SDK is stubbed, ChatSee is not.
|
|
97
|
+
That matters more here than anywhere else: a real turn costs about $0.027, so
|
|
98
|
+
finding a capture bug by running one would be slow and expensive.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install -e python/connectors/_shared
|
|
102
|
+
pip install -e python/connectors/ai_sdk/claude_agent_sdk
|
|
103
|
+
pytest python/connectors/ai_sdk/claude_agent_sdk/tests -q
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Known gap
|
|
107
|
+
|
|
108
|
+
Turn-level `duration_ms` comes from the tracker's own wall clock, and all four
|
|
109
|
+
tracker calls are made together once the turn is known to be complete — so it
|
|
110
|
+
reads near zero. The honest latency is on the model call, from the SDK's
|
|
111
|
+
`duration_api_ms`. Fixing it properly means letting `end_turn` accept a
|
|
112
|
+
measured duration, which is a core-SDK change and not this connector's to make.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# chatsee-claude-agent-sdk
|
|
2
|
+
|
|
3
|
+
ChatSee connector for the **Claude Agent SDK** — Anthropic's agent framework,
|
|
4
|
+
the one that powers Claude Code.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install chatsee-claude-agent-sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from chatsee_claude_agent_sdk import instrument_claude_agent_sdk
|
|
12
|
+
|
|
13
|
+
chatsee = instrument_claude_agent_sdk(agent_id="...", tenant_id="...")
|
|
14
|
+
|
|
15
|
+
async for message in chatsee.query(prompt="where is my order", options=opts):
|
|
16
|
+
...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`chatsee-ai` is installed for you as a dependency.
|
|
20
|
+
|
|
21
|
+
## Not the same thing as `chatsee-anthropic`
|
|
22
|
+
|
|
23
|
+
| | `chatsee-anthropic` | this |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| Package it hooks | `anthropic` | `claude-agent-sdk` |
|
|
26
|
+
| Who writes the agent loop | the customer | Anthropic |
|
|
27
|
+
| What we hook | `client.messages.create` | the `query()` message stream |
|
|
28
|
+
|
|
29
|
+
Use `chatsee-anthropic` when they call the Messages API and run their own
|
|
30
|
+
tool-use loop. Use this when they call `query()` and let the SDK run it.
|
|
31
|
+
|
|
32
|
+
## Why a handle rather than a one-liner
|
|
33
|
+
|
|
34
|
+
There is no client object to patch — the entry point is a module-level
|
|
35
|
+
`query()`. So `instrument_claude_agent_sdk` returns a handle whose `.query` is
|
|
36
|
+
a drop-in: every message still reaches the caller, unchanged and in order,
|
|
37
|
+
while the turn is assembled alongside.
|
|
38
|
+
|
|
39
|
+
`claude_agent_sdk.query` is replaced too, so code calling it **through the
|
|
40
|
+
module** needs no change. Code that did `from claude_agent_sdk import query`
|
|
41
|
+
already holds its own reference to the original and cannot be reached that way
|
|
42
|
+
— that code uses the handle.
|
|
43
|
+
|
|
44
|
+
## What it captures
|
|
45
|
+
|
|
46
|
+
Per turn: the prompt, the final answer, the system prompt, every tool call with
|
|
47
|
+
its arguments and result, and **one model call per model**, with tokens, cached
|
|
48
|
+
tokens and a real dollar cost.
|
|
49
|
+
|
|
50
|
+
This SDK is the only one that states its own cost (`total_cost_usd`), so that
|
|
51
|
+
figure is passed through rather than estimated. Nothing here invents a price,
|
|
52
|
+
and a turn's cost is never divided across models — a split we made up would
|
|
53
|
+
look like measurement.
|
|
54
|
+
|
|
55
|
+
## Three things this framework does that nothing else does
|
|
56
|
+
|
|
57
|
+
**The CLI's system prompt is enormous.** Claude Code injects roughly 21k cached
|
|
58
|
+
tokens on every call, so a one-line "hello" costs around $0.027. Those land in
|
|
59
|
+
`cache_read_input_tokens`, not `input_tokens` — both are captured, or the turn
|
|
60
|
+
looks far cheaper than the bill.
|
|
61
|
+
|
|
62
|
+
**The CLI calls its own tools.** It loads tools on demand and emits a
|
|
63
|
+
`ToolSearch` tool-use alongside the agent's real one. Those are filtered: a
|
|
64
|
+
support agent that never touched a search tool must not show one in its trace.
|
|
65
|
+
|
|
66
|
+
**Tool names arrive MCP-qualified.** Custom tools reach the model as
|
|
67
|
+
`mcp__<server>__<tool>`. The customer named it `get_order_status`, so the
|
|
68
|
+
prefix is stripped and that is what the screens say.
|
|
69
|
+
|
|
70
|
+
## Guarantees
|
|
71
|
+
|
|
72
|
+
* **Fails open.** A failure inside capture is logged and swallowed.
|
|
73
|
+
* **Never consumes the stream.** Every message is forwarded unchanged, in order.
|
|
74
|
+
* **Idempotent.** Instrumenting twice does not capture twice.
|
|
75
|
+
* **Concurrency-safe.** State is per `query()` call, not per handle.
|
|
76
|
+
|
|
77
|
+
All four are enforced by the contract suite, not just intended.
|
|
78
|
+
|
|
79
|
+
## Tests
|
|
80
|
+
|
|
81
|
+
No network, no API key, no CLI subprocess — the SDK is stubbed, ChatSee is not.
|
|
82
|
+
That matters more here than anywhere else: a real turn costs about $0.027, so
|
|
83
|
+
finding a capture bug by running one would be slow and expensive.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install -e python/connectors/_shared
|
|
87
|
+
pip install -e python/connectors/ai_sdk/claude_agent_sdk
|
|
88
|
+
pytest python/connectors/ai_sdk/claude_agent_sdk/tests -q
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Known gap
|
|
92
|
+
|
|
93
|
+
Turn-level `duration_ms` comes from the tracker's own wall clock, and all four
|
|
94
|
+
tracker calls are made together once the turn is known to be complete — so it
|
|
95
|
+
reads near zero. The honest latency is on the model call, from the SDK's
|
|
96
|
+
`duration_api_ms`. Fixing it properly means letting `end_turn` accept a
|
|
97
|
+
measured duration, which is a core-SDK change and not this connector's to make.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChatSee connector for the Claude Agent SDK.
|
|
3
|
+
|
|
4
|
+
from chatsee_claude_agent_sdk import instrument_claude_agent_sdk
|
|
5
|
+
|
|
6
|
+
chatsee = instrument_claude_agent_sdk(agent_id="...", tenant_id="...")
|
|
7
|
+
|
|
8
|
+
async for message in chatsee.query(prompt="where is my order", options=opts):
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
Unlike the Messages API connector, there is no client object to patch — the
|
|
12
|
+
Agent SDK's entry point is a module-level ``query()``. So this returns a handle
|
|
13
|
+
whose ``.query`` is a drop-in for it: every message still reaches the caller,
|
|
14
|
+
unchanged and in order, while the turn is assembled alongside and sent when the
|
|
15
|
+
terminal ``ResultMessage`` arrives.
|
|
16
|
+
|
|
17
|
+
``claude_agent_sdk.query`` is also replaced, so code that calls it through the
|
|
18
|
+
module is captured without changing a line. Code that did
|
|
19
|
+
``from claude_agent_sdk import query`` already holds its own reference to the
|
|
20
|
+
original function and cannot be reached that way — use the handle.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any, AsyncIterator, Optional
|
|
26
|
+
|
|
27
|
+
from ._capture import (
|
|
28
|
+
FRAMEWORK,
|
|
29
|
+
TurnAccumulator,
|
|
30
|
+
UsageLedger,
|
|
31
|
+
model_calls_from,
|
|
32
|
+
system_prompt_of,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
__all__ = ["instrument_claude_agent_sdk", "ChatseeClaudeAgentSDK", "__version__"]
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
_MARKER = "_chatsee_instrumented"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _framework_version() -> str:
|
|
44
|
+
try:
|
|
45
|
+
import claude_agent_sdk
|
|
46
|
+
|
|
47
|
+
return getattr(claude_agent_sdk, "__version__", "unknown")
|
|
48
|
+
except Exception:
|
|
49
|
+
return "unknown"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _attr(obj: Any, name: str, default: Any = None) -> Any:
|
|
53
|
+
if isinstance(obj, dict):
|
|
54
|
+
return obj.get(name, default)
|
|
55
|
+
return getattr(obj, name, default)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_result(message: Any) -> bool:
|
|
59
|
+
"""The terminal message, identified by shape rather than by import.
|
|
60
|
+
|
|
61
|
+
Importing ResultMessage would make the connector unimportable without the
|
|
62
|
+
SDK installed, which the offline tests rely on not being the case.
|
|
63
|
+
"""
|
|
64
|
+
return type(message).__name__ == "ResultMessage" or (
|
|
65
|
+
_attr(message, "subtype") is not None and _attr(message, "total_cost_usd") is not None
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _kind(message: Any) -> str:
|
|
70
|
+
return type(message).__name__
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ChatseeClaudeAgentSDK:
|
|
74
|
+
"""Handle returned by :func:`instrument_claude_agent_sdk`."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, tracker: Any, query_fn: Any, user_id: Optional[str] = None) -> None:
|
|
77
|
+
self._tracker = tracker
|
|
78
|
+
self._query = query_fn
|
|
79
|
+
self._user_id = user_id
|
|
80
|
+
# Per-session running totals, so cumulative figures become per-turn.
|
|
81
|
+
self._ledger = UsageLedger()
|
|
82
|
+
|
|
83
|
+
# -- the drop-in -------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def query(self, *args: Any, **kwargs: Any) -> AsyncIterator[Any]:
|
|
86
|
+
"""A drop-in for the SDK's ``query()``."""
|
|
87
|
+
return self._stream(*args, **kwargs)
|
|
88
|
+
|
|
89
|
+
async def _stream(self, *args: Any, **kwargs: Any) -> AsyncIterator[Any]:
|
|
90
|
+
prompt = kwargs.get("prompt")
|
|
91
|
+
if prompt is None and args:
|
|
92
|
+
prompt = args[0]
|
|
93
|
+
options = kwargs.get("options")
|
|
94
|
+
|
|
95
|
+
# One accumulator per call, never shared, so concurrent queries cannot
|
|
96
|
+
# contaminate each other.
|
|
97
|
+
turn = TurnAccumulator()
|
|
98
|
+
|
|
99
|
+
async for message in self._query(*args, **kwargs):
|
|
100
|
+
try:
|
|
101
|
+
kind = _kind(message)
|
|
102
|
+
if kind == "AssistantMessage":
|
|
103
|
+
turn.observe_assistant(message)
|
|
104
|
+
elif kind == "UserMessage":
|
|
105
|
+
turn.observe_user(message)
|
|
106
|
+
elif _is_result(message):
|
|
107
|
+
self._emit(prompt, options, turn, message)
|
|
108
|
+
except Exception as exc: # noqa: BLE001 - never break the agent
|
|
109
|
+
logger.error("chatsee-claude-agent-sdk capture failed: %s", exc,
|
|
110
|
+
exc_info=True)
|
|
111
|
+
yield message # forwarded unchanged, in order
|
|
112
|
+
|
|
113
|
+
# -- emitting ----------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def _metadata(self) -> dict:
|
|
116
|
+
return {
|
|
117
|
+
"framework": FRAMEWORK,
|
|
118
|
+
"framework_version": _framework_version(),
|
|
119
|
+
"connector_version": __version__,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def _emit(self, prompt: Any, options: Any, turn: TurnAccumulator,
|
|
123
|
+
result_message: Any) -> None:
|
|
124
|
+
"""All four tracker calls, together, when the turn is known to be over.
|
|
125
|
+
|
|
126
|
+
Emitted in one go rather than opening the turn when the stream starts:
|
|
127
|
+
the tracker holds a single turn at a time, so a start_turn left open
|
|
128
|
+
across an await would be clobbered by any concurrent query.
|
|
129
|
+
"""
|
|
130
|
+
tracker = self._tracker
|
|
131
|
+
answer = _attr(result_message, "result") or ""
|
|
132
|
+
|
|
133
|
+
tracker.start_turn(
|
|
134
|
+
prompt if isinstance(prompt, str) else "",
|
|
135
|
+
metadata=self._metadata(),
|
|
136
|
+
system_prompt=system_prompt_of(options),
|
|
137
|
+
user_id=self._user_id,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
for call in turn.resolved_tool_calls():
|
|
141
|
+
tracker.log_tool_call(call["tool_name"], call["arguments"],
|
|
142
|
+
result=call["result"])
|
|
143
|
+
|
|
144
|
+
# One entry per model the turn used. The SDK states a real dollar cost,
|
|
145
|
+
# so it is passed through rather than estimated -- nothing here invents
|
|
146
|
+
# a price. duration_api_ms is the time actually spent in the API, which
|
|
147
|
+
# is the honest per-call latency.
|
|
148
|
+
api_ms = _attr(result_message, "duration_api_ms")
|
|
149
|
+
session_id = _attr(result_message, "session_id")
|
|
150
|
+
calls, cumulative = model_calls_from(result_message)
|
|
151
|
+
|
|
152
|
+
# model_usage and total_cost_usd are running totals for the query()
|
|
153
|
+
# call, not this turn's share. One turn per call makes them the same
|
|
154
|
+
# number, which is why it never shows in the ordinary case; a
|
|
155
|
+
# streaming-input or resumed session runs several turns through one
|
|
156
|
+
# total, and recording it raw makes turn three report turns one, two
|
|
157
|
+
# and three.
|
|
158
|
+
if cumulative:
|
|
159
|
+
calls = self._ledger.per_turn_models(session_id, calls)
|
|
160
|
+
total_cost = None # already carried per model
|
|
161
|
+
else:
|
|
162
|
+
# The aggregate usage is per-turn, but the cost beside it is not.
|
|
163
|
+
total_cost = self._ledger.per_turn_cost(
|
|
164
|
+
session_id, _attr(result_message, "total_cost_usd"))
|
|
165
|
+
|
|
166
|
+
for entry in calls:
|
|
167
|
+
tracker.log_model_call(
|
|
168
|
+
model=entry.get("model") or turn.model,
|
|
169
|
+
provider="anthropic",
|
|
170
|
+
prompt_tokens=entry.get("prompt_tokens"),
|
|
171
|
+
completion_tokens=entry.get("completion_tokens"),
|
|
172
|
+
cached_tokens=entry.get("cached_tokens"),
|
|
173
|
+
# A per-model cost when the SDK broke it down, otherwise the
|
|
174
|
+
# turn's cost on the single entry. Never divided up: a split
|
|
175
|
+
# we invented would look like measurement.
|
|
176
|
+
cost_usd=entry.get("cost_usd") if entry.get("cost_usd") is not None
|
|
177
|
+
else (total_cost if len(calls) == 1 else None),
|
|
178
|
+
duration_ms=api_ms if len(calls) == 1 else None,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if _attr(result_message, "is_error"):
|
|
182
|
+
tracker.log_exception(
|
|
183
|
+
"claude-agent-sdk reported subtype=%s" % _attr(result_message, "subtype")
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
tracker.end_turn(answer)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def instrument_claude_agent_sdk(
|
|
190
|
+
agent_id: Optional[str] = None,
|
|
191
|
+
tenant_id: Optional[str] = None,
|
|
192
|
+
user_id: Optional[str] = None,
|
|
193
|
+
session_id: Optional[str] = None,
|
|
194
|
+
api_base_url: Optional[str] = None,
|
|
195
|
+
_tracker: Any = None,
|
|
196
|
+
_query: Any = None,
|
|
197
|
+
) -> ChatseeClaudeAgentSDK:
|
|
198
|
+
"""Capture every turn the Claude Agent SDK runs.
|
|
199
|
+
|
|
200
|
+
``_tracker`` and ``_query`` are private and exist so the contract tests can
|
|
201
|
+
inject a fake tracker and a stub SDK. Customers never pass them.
|
|
202
|
+
"""
|
|
203
|
+
if _tracker is None:
|
|
204
|
+
from chatsee import ChatseeTracker
|
|
205
|
+
|
|
206
|
+
if not agent_id or not tenant_id:
|
|
207
|
+
raise ValueError("agent_id and tenant_id are required.")
|
|
208
|
+
_tracker = ChatseeTracker(
|
|
209
|
+
agent_id=agent_id,
|
|
210
|
+
tenant_id=tenant_id,
|
|
211
|
+
user_id=user_id,
|
|
212
|
+
session_id=session_id,
|
|
213
|
+
api_base_url=api_base_url,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
module = None
|
|
217
|
+
if _query is None:
|
|
218
|
+
import claude_agent_sdk as module
|
|
219
|
+
|
|
220
|
+
_query = getattr(module, _MARKER + "_original", None) or module.query
|
|
221
|
+
|
|
222
|
+
handle = ChatseeClaudeAgentSDK(_tracker, _query, user_id)
|
|
223
|
+
|
|
224
|
+
# Also replace the module-level query, so callers using
|
|
225
|
+
# `claude_agent_sdk.query(...)` need no change at all. Guarded, because
|
|
226
|
+
# instrumenting twice must not wrap twice.
|
|
227
|
+
if module is not None and not getattr(module, _MARKER, False):
|
|
228
|
+
setattr(module, _MARKER + "_original", _query)
|
|
229
|
+
module.query = handle.query
|
|
230
|
+
setattr(module, _MARKER, True)
|
|
231
|
+
|
|
232
|
+
return handle
|