@pentatonic-ai/ai-agent-sdk 0.3.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pentatonic Ltd
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.
package/README.md ADDED
@@ -0,0 +1,401 @@
1
+ # @pentatonic-ai/ai-agent-sdk
2
+
3
+ LLM observability SDK — track token usage, tool calls, and conversations via [Pentatonic TES](https://api.pentatonic.com).
4
+
5
+ Provider-agnostic: automatically wraps OpenAI, Anthropic, and Cloudflare Workers AI clients. Available for both **JavaScript** and **Python**.
6
+
7
+ ## Getting Started
8
+
9
+ ### 1. Create an account and get your API key
10
+
11
+ ```bash
12
+ npx @pentatonic-ai/ai-agent-sdk init
13
+ ```
14
+
15
+ This will walk you through:
16
+ - Creating a Pentatonic account (email, company name, password)
17
+ - Choosing a data region (EU or US)
18
+ - Email verification
19
+ - Generating your API key
20
+
21
+ At the end you'll see your credentials:
22
+
23
+ ```
24
+ TES_ENDPOINT=https://api.pentatonic.com
25
+ TES_CLIENT_ID=your-company
26
+ TES_API_KEY=tes_your-company_xxxxx
27
+ ```
28
+
29
+ Add these to your environment (`.env`, secrets manager, etc.) and the CLI will install the SDK for you.
30
+
31
+ ### 2. Or install manually
32
+
33
+ If you already have an account, install the SDK directly:
34
+
35
+ ```bash
36
+ npm install @pentatonic-ai/ai-agent-sdk
37
+ ```
38
+
39
+ ```bash
40
+ pip install pentatonic-ai-agent-sdk
41
+ ```
42
+
43
+ You can create API keys in the [Pentatonic dashboard](https://api.pentatonic.com).
44
+
45
+ ## Quick Start
46
+
47
+ #### JavaScript
48
+
49
+ ```js
50
+ import { TESClient } from "@pentatonic-ai/ai-agent-sdk";
51
+
52
+ const tes = new TESClient({
53
+ clientId: process.env.TES_CLIENT_ID,
54
+ apiKey: process.env.TES_API_KEY,
55
+ endpoint: process.env.TES_ENDPOINT,
56
+ });
57
+ ```
58
+
59
+ #### Python
60
+
61
+ ```python
62
+ from pentatonic_agent_events import TESClient
63
+ import os
64
+
65
+ tes = TESClient(
66
+ client_id=os.environ["TES_CLIENT_ID"],
67
+ api_key=os.environ["TES_API_KEY"],
68
+ endpoint=os.environ["TES_ENDPOINT"],
69
+ )
70
+ ```
71
+
72
+ ### Wrap any LLM client (automatic tracking)
73
+
74
+ `tes.wrap()` auto-detects your client and intercepts every call — each one emits a `CHAT_TURN` event automatically. Pass an optional `sessionId` to link events from the same conversation, and `metadata` to attach custom fields.
75
+
76
+ #### JavaScript — OpenAI
77
+
78
+ ```js
79
+ import OpenAI from "openai";
80
+
81
+ const ai = tes.wrap(new OpenAI(), { sessionId: "conv-123", metadata: { userId: "u_1" } });
82
+
83
+ // Every create() call automatically emits a CHAT_TURN event
84
+ const result = await ai.chat.completions.create({
85
+ model: "gpt-4o",
86
+ messages: [{ role: "user", content: "Hello!" }],
87
+ });
88
+
89
+ ai.sessionId; // "conv-123" — or auto-generated UUID if not provided
90
+ ```
91
+
92
+ #### Python — OpenAI
93
+
94
+ ```python
95
+ from openai import OpenAI
96
+
97
+ ai = tes.wrap(OpenAI(), session_id="conv-123", metadata={"user_id": "u_1"})
98
+
99
+ # Every create() call automatically emits a CHAT_TURN event
100
+ result = ai.chat.completions.create(
101
+ model="gpt-4o",
102
+ messages=[{"role": "user", "content": "Hello!"}],
103
+ )
104
+
105
+ ai.session_id # "conv-123" — or auto-generated UUID if not provided
106
+ ```
107
+
108
+ #### JavaScript — Anthropic
109
+
110
+ ```js
111
+ import Anthropic from "@anthropic-ai/sdk";
112
+
113
+ const claude = tes.wrap(new Anthropic());
114
+
115
+ const result = await claude.messages.create({
116
+ model: "claude-sonnet-4-6-20250514",
117
+ max_tokens: 1024,
118
+ messages: [{ role: "user", content: "Hello!" }],
119
+ });
120
+ ```
121
+
122
+ #### Python — Anthropic
123
+
124
+ ```python
125
+ from anthropic import Anthropic
126
+
127
+ claude = tes.wrap(Anthropic())
128
+
129
+ result = claude.messages.create(
130
+ model="claude-sonnet-4-6-20250514",
131
+ max_tokens=1024,
132
+ messages=[{"role": "user", "content": "Hello!"}],
133
+ )
134
+ ```
135
+
136
+ #### JavaScript — Cloudflare Workers AI
137
+
138
+ ```js
139
+ // Cloudflare Workers AI binding
140
+ const ai = tes.wrap(env.AI, { sessionId: sid, metadata: { shop: shopDomain } });
141
+
142
+ // run() is intercepted automatically
143
+ const result = await ai.run("@cf/meta/llama-3.1-8b-instruct", {
144
+ messages: [{ role: "user", content: "Hello!" }],
145
+ });
146
+ ```
147
+
148
+ > **Note:** Workers AI is a Cloudflare-specific binding and is only available in JavaScript.
149
+
150
+ ### Tool-calling loops
151
+
152
+ For multi-round tool loops, just keep calling the wrapped client. Each `create()`/`run()` call emits its own event, and they're linked by `sessionId`. The dashboard aggregates tokens, tool calls, and turns per session automatically.
153
+
154
+ #### JavaScript
155
+
156
+ ```js
157
+ const ai = tes.wrap(new OpenAI(), { sessionId: "conv-101" });
158
+
159
+ // Round 1: AI requests a tool call — emits event with tool_calls
160
+ const r1 = await ai.chat.completions.create({
161
+ model: "gpt-4o",
162
+ messages: [{ role: "user", content: "Find me running shoes" }],
163
+ tools: [searchTool],
164
+ });
165
+
166
+ // Execute tool, feed results back...
167
+
168
+ // Round 2: AI responds with final answer — emits another event
169
+ const r2 = await ai.chat.completions.create({
170
+ model: "gpt-4o",
171
+ messages: [...messages, { role: "tool", content: toolResult }],
172
+ });
173
+
174
+ // That's it. No manual emit needed. Both events share sessionId "conv-101".
175
+ ```
176
+
177
+ #### Python
178
+
179
+ ```python
180
+ ai = tes.wrap(OpenAI(), session_id="conv-101")
181
+
182
+ r1 = ai.chat.completions.create(
183
+ model="gpt-4o",
184
+ messages=[{"role": "user", "content": "Find me running shoes"}],
185
+ tools=[search_tool],
186
+ )
187
+
188
+ # Execute tool, feed results back...
189
+
190
+ r2 = ai.chat.completions.create(
191
+ model="gpt-4o",
192
+ messages=[*messages, {"role": "tool", "content": tool_result}],
193
+ )
194
+
195
+ # No manual emit needed.
196
+ ```
197
+
198
+ ### Manual session (full control)
199
+
200
+ If you don't want to use `tes.wrap()`, create a session directly:
201
+
202
+ #### JavaScript
203
+
204
+ ```js
205
+ const session = tes.session({
206
+ sessionId: "conv-123",
207
+ metadata: { userId: "u_456" },
208
+ });
209
+
210
+ // Call your LLM however you like
211
+ const response = await openai.chat.completions.create({
212
+ model: "gpt-4o",
213
+ messages: [{ role: "user", content: "What is 2+2?" }],
214
+ });
215
+
216
+ // Record the response (accumulates tokens, tool calls, model)
217
+ session.record(response);
218
+
219
+ // Emit when the turn is complete
220
+ await session.emitChatTurn({
221
+ userMessage: "What is 2+2?",
222
+ assistantResponse: response.choices[0].message.content,
223
+ });
224
+ ```
225
+
226
+ #### Python
227
+
228
+ ```python
229
+ session = tes.session(
230
+ session_id="conv-123",
231
+ metadata={"user_id": "u_456"},
232
+ )
233
+
234
+ response = openai.chat.completions.create(
235
+ model="gpt-4o",
236
+ messages=[{"role": "user", "content": "What is 2+2?"}],
237
+ )
238
+
239
+ session.record(response)
240
+
241
+ session.emit_chat_turn(
242
+ user_message="What is 2+2?",
243
+ assistant_response=response["choices"][0]["message"]["content"],
244
+ )
245
+ ```
246
+
247
+ ## API Reference
248
+
249
+ ### `TESClient`
250
+
251
+ Creates a new client.
252
+
253
+ #### JavaScript
254
+
255
+ ```js
256
+ new TESClient({ clientId, apiKey, endpoint, headers?, userId?, captureContent?, maxContentLength? })
257
+ ```
258
+
259
+ #### Python
260
+
261
+ ```python
262
+ TESClient(client_id, api_key, endpoint, headers=None, user_id=None, capture_content=True, max_content_length=4096)
263
+ ```
264
+
265
+ | Param (JS / Python) | Type | Default | Description |
266
+ |----------------------|------|---------|-------------|
267
+ | `clientId` / `client_id` | `string` | *required* | Your application/tenant identifier |
268
+ | `apiKey` / `api_key` | `string` | *required* | TES service API key (sent as `x-service-key` header) |
269
+ | `endpoint` / `endpoint` | `string` | *required* | TES instance URL (must be `https://`, except `localhost` for dev) |
270
+ | `headers` / `headers` | `object` / `dict` | `{}` | Additional headers to include in every request |
271
+ | `userId` / `user_id` | `string` | `null` / `None` | Optional user identifier — included as `data.attributes.userId` on every event. Enables user-scoped memory and attribution. |
272
+ | `captureContent` / `capture_content` | `boolean` / `bool` | `true` / `True` | Whether to include message content in events |
273
+ | `maxContentLength` / `max_content_length` | `number` / `int` | `4096` | Truncate content beyond this length |
274
+
275
+ ### `tes.wrap(client, opts?)`
276
+
277
+ Returns a Proxy (JS) or wrapper (Python) around any supported LLM client. Every intercepted call emits a `CHAT_TURN` event automatically.
278
+
279
+ #### JavaScript
280
+
281
+ ```js
282
+ const ai = tes.wrap(client, { sessionId, userId, metadata });
283
+ ```
284
+
285
+ #### Python
286
+
287
+ ```python
288
+ ai = tes.wrap(client, session_id=None, user_id=None, metadata=None)
289
+ ```
290
+
291
+ | Option (JS / Python) | Type | Default | Description |
292
+ |----------------------|------|---------|-------------|
293
+ | `sessionId` / `session_id` | `string` | `crypto.randomUUID()` / `uuid.uuid4()` | Links events from the same conversation |
294
+ | `userId` / `user_id` | `string` | Inherits from client | Override the user identifier for this wrapped instance |
295
+ | `metadata` / `metadata` | `object` / `dict` | `{}` | Custom fields included in every emitted event |
296
+
297
+ Auto-detects the provider:
298
+
299
+ | Client | Detection | Intercepted method |
300
+ |--------|-----------|-------------------|
301
+ | OpenAI | `client.chat.completions.create` | `chat.completions.create()` |
302
+ | Anthropic | `client.messages.create` | `messages.create()` |
303
+ | Workers AI | `client.run` (JS only) | `run()` |
304
+
305
+ All other methods/properties pass through unchanged. The wrapped client exposes `ai.sessionId` (JS) or `ai.session_id` (Python).
306
+
307
+ ### `tes.session(opts?)`
308
+
309
+ Returns a `Session` instance.
310
+
311
+ | Option (JS / Python) | Type | Default | Description |
312
+ |----------------------|------|---------|-------------|
313
+ | `sessionId` / `session_id` | `string` | `crypto.randomUUID()` / `uuid.uuid4()` | Conversation/session identifier |
314
+ | `metadata` / `metadata` | `object` / `dict` | `{}` | Extra fields included in every emitted event |
315
+
316
+ ### `session.record(rawResponse)`
317
+
318
+ Normalizes an LLM response and accumulates token usage, tool calls, and model info. Accepts responses from any supported provider. Returns the normalized response.
319
+
320
+ ### `session.emitChatTurn()` / `session.emit_chat_turn()`
321
+
322
+ Sends a `CHAT_TURN` event to TES with accumulated usage data, then resets counters.
323
+
324
+ | Param (JS / Python) | Type | Description |
325
+ |---------------------|------|-------------|
326
+ | `userMessage` / `user_message` | `string` | The user's message |
327
+ | `assistantResponse` / `assistant_response` | `string` | The assistant's response |
328
+ | `turnNumber` / `turn_number` | `number` / `int` | Optional turn number |
329
+
330
+ ### `session.emitToolUse()` / `session.emit_tool_use()`
331
+
332
+ Sends a `TOOL_USE` event for individual tool invocations.
333
+
334
+ | Param (JS / Python) | Type | Description |
335
+ |---------------------|------|-------------|
336
+ | `tool` / `tool` | `string` | Tool name |
337
+ | `args` / `args` | `object` / `dict` | Tool arguments |
338
+ | `resultSummary` / `result_summary` | `string` | Optional result summary |
339
+ | `durationMs` / `duration_ms` | `number` / `int` | Optional duration in milliseconds |
340
+ | `turnNumber` / `turn_number` | `number` / `int` | Optional turn number |
341
+
342
+ ### `session.emitSessionStart()` / `session.emit_session_start()`
343
+
344
+ Sends a `SESSION_START` event.
345
+
346
+ ### `session.totalUsage` / `session.total_usage`
347
+
348
+ Returns current accumulated usage: `{ prompt_tokens, completion_tokens, total_tokens, ai_rounds }`.
349
+
350
+ ### `normalizeResponse(raw)` / `normalize_response(raw)`
351
+
352
+ Standalone utility to normalize any LLM response into a consistent shape:
353
+
354
+ #### JavaScript
355
+
356
+ ```js
357
+ import { normalizeResponse } from "@pentatonic-ai/ai-agent-sdk";
358
+
359
+ const normalized = normalizeResponse(openaiResponse);
360
+ // { content, model, usage: { prompt_tokens, completion_tokens }, toolCalls: [{ tool, args }] }
361
+ ```
362
+
363
+ #### Python
364
+
365
+ ```python
366
+ from pentatonic_agent_events import normalize_response
367
+
368
+ normalized = normalize_response(openai_response)
369
+ # { "content", "model", "usage": { "prompt_tokens", "completion_tokens" }, "tool_calls": [{ "tool", "args" }] }
370
+ ```
371
+
372
+ > **Note:** In Python, the normalized response uses `tool_calls` (snake_case) instead of `toolCalls` (camelCase).
373
+
374
+ ## Events Emitted
375
+
376
+ All events are sent to the TES GraphQL API (`emitEvent` mutation) authenticated via `x-service-key` and `x-client-id` headers.
377
+
378
+ | Event Type | Entity Type | When |
379
+ |------------|-------------|------|
380
+ | `CHAT_TURN` | `conversation` | Every `create()`/`run()` call via `wrap()`, or manually via `session.emitChatTurn()` |
381
+ | `TOOL_USE` | `conversation` | Via `session.emitToolUse()` (manual only) |
382
+ | `SESSION_START` | `conversation` | Via `session.emitSessionStart()` (manual only) |
383
+
384
+ ## Supported Providers
385
+
386
+ | Provider | Auto-wrap | Manual session | Response normalization |
387
+ |----------|-----------|---------------|----------------------|
388
+ | **OpenAI** (and compatible: Azure, Groq, Together, Mistral) | JS + Python | JS + Python | JS + Python |
389
+ | **Anthropic** | JS + Python | JS + Python | JS + Python |
390
+ | **Cloudflare Workers AI** | JS only | JS only | JS + Python |
391
+
392
+ ## Security
393
+
394
+ - **HTTPS enforced:** The SDK rejects non-HTTPS endpoints (except `localhost` for development)
395
+ - **API key protection:** Stored as a non-enumerable property (JS) or private attribute (Python) — won't appear in `JSON.stringify`, `repr()`, or error reporters
396
+ - **Content controls:** Set `captureContent: false` (JS) or `capture_content=False` (Python) to omit message content from events, or use `maxContentLength` / `max_content_length` to truncate
397
+ - **No runtime dependencies:** Both the JavaScript and Python SDKs have zero external runtime dependencies
398
+
399
+ ## License
400
+
401
+ MIT