@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 +21 -0
- package/README.md +401 -0
- package/bin/cli.js +371 -0
- package/build.js +23 -0
- package/dist/index.cjs +699 -0
- package/dist/index.js +677 -0
- package/dist/pentatonic_agent_events-0.2.0b1-py3-none-any.whl +0 -0
- package/dist/pentatonic_agent_events-0.2.0b1.tar.gz +0 -0
- package/dist/pentatonic_agent_events-0.3.0b1-py3-none-any.whl +0 -0
- package/dist/pentatonic_agent_events-0.3.0b1.tar.gz +0 -0
- package/dist/pentatonic_agent_events-0.3.0b2-py3-none-any.whl +0 -0
- package/dist/pentatonic_agent_events-0.3.0b2.tar.gz +0 -0
- package/dist/pentatonic_agent_events-0.3.0b3-py3-none-any.whl +0 -0
- package/dist/pentatonic_agent_events-0.3.0b3.tar.gz +0 -0
- package/package.json +64 -0
- package/src/client.js +60 -0
- package/src/index.js +4 -0
- package/src/normalizer.js +111 -0
- package/src/session.js +181 -0
- package/src/tracking.js +119 -0
- package/src/transport.js +48 -0
- package/src/wrapper.js +329 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/index.js
|
|
20
|
+
var src_exports = {};
|
|
21
|
+
__export(src_exports, {
|
|
22
|
+
Session: () => Session,
|
|
23
|
+
TESClient: () => TESClient,
|
|
24
|
+
buildTrackUrl: () => buildTrackUrl,
|
|
25
|
+
normalizeResponse: () => normalizeResponse,
|
|
26
|
+
rewriteUrls: () => rewriteUrls,
|
|
27
|
+
signPayload: () => signPayload,
|
|
28
|
+
verifyPayload: () => verifyPayload
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(src_exports);
|
|
31
|
+
|
|
32
|
+
// src/normalizer.js
|
|
33
|
+
function normalizeResponse(raw) {
|
|
34
|
+
if (!raw || typeof raw !== "object") {
|
|
35
|
+
return empty();
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(raw.choices)) {
|
|
38
|
+
return normalizeOpenAI(raw);
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(raw.content) && raw.content[0]?.type) {
|
|
41
|
+
return normalizeAnthropic(raw);
|
|
42
|
+
}
|
|
43
|
+
if (typeof raw.response === "string" || raw.tool_calls && !raw.choices) {
|
|
44
|
+
return normalizeWorkersAI(raw);
|
|
45
|
+
}
|
|
46
|
+
return empty();
|
|
47
|
+
}
|
|
48
|
+
function empty() {
|
|
49
|
+
return {
|
|
50
|
+
content: "",
|
|
51
|
+
model: null,
|
|
52
|
+
usage: { prompt_tokens: 0, completion_tokens: 0 },
|
|
53
|
+
toolCalls: []
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function normalizeOpenAI(raw) {
|
|
57
|
+
const message = raw.choices?.[0]?.message || {};
|
|
58
|
+
const usage = raw.usage || {};
|
|
59
|
+
const rawToolCalls = message.tool_calls?.length ? message.tool_calls : raw.tool_calls || [];
|
|
60
|
+
const toolCalls = rawToolCalls.map((tc) => ({
|
|
61
|
+
tool: tc.function?.name || tc.name,
|
|
62
|
+
args: parseArgs(tc.function?.arguments || tc.arguments)
|
|
63
|
+
}));
|
|
64
|
+
return {
|
|
65
|
+
content: message.content || "",
|
|
66
|
+
model: raw.model || null,
|
|
67
|
+
usage: {
|
|
68
|
+
prompt_tokens: usage.prompt_tokens || 0,
|
|
69
|
+
completion_tokens: usage.completion_tokens || 0
|
|
70
|
+
},
|
|
71
|
+
toolCalls
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function normalizeAnthropic(raw) {
|
|
75
|
+
const usage = raw.usage || {};
|
|
76
|
+
let content = "";
|
|
77
|
+
const toolCalls = [];
|
|
78
|
+
for (const block of raw.content) {
|
|
79
|
+
if (block.type === "text") {
|
|
80
|
+
content += block.text;
|
|
81
|
+
} else if (block.type === "tool_use") {
|
|
82
|
+
toolCalls.push({ tool: block.name, args: block.input || {} });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
content,
|
|
87
|
+
model: raw.model || null,
|
|
88
|
+
usage: {
|
|
89
|
+
prompt_tokens: usage.input_tokens || 0,
|
|
90
|
+
completion_tokens: usage.output_tokens || 0
|
|
91
|
+
},
|
|
92
|
+
toolCalls
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function normalizeWorkersAI(raw) {
|
|
96
|
+
const usage = raw.usage || {};
|
|
97
|
+
const toolCalls = (raw.tool_calls || []).map((tc) => ({
|
|
98
|
+
tool: tc.function?.name || tc.name,
|
|
99
|
+
args: parseArgs(tc.function?.arguments || tc.arguments || {})
|
|
100
|
+
}));
|
|
101
|
+
return {
|
|
102
|
+
content: raw.response || "",
|
|
103
|
+
model: raw.model || null,
|
|
104
|
+
usage: {
|
|
105
|
+
prompt_tokens: usage.prompt_tokens || 0,
|
|
106
|
+
completion_tokens: usage.completion_tokens || 0
|
|
107
|
+
},
|
|
108
|
+
toolCalls
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function parseArgs(args) {
|
|
112
|
+
if (typeof args === "string") {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(args);
|
|
115
|
+
} catch {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return args || {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/transport.js
|
|
123
|
+
var EMIT_EVENT_MUTATION = `
|
|
124
|
+
mutation EmitEvent($input: EventInput!) {
|
|
125
|
+
emitEvent(input: $input) {
|
|
126
|
+
success
|
|
127
|
+
eventId
|
|
128
|
+
message
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
async function sendEvent({ endpoint, apiKey, clientId, userId, headers }, input, fetchFn) {
|
|
133
|
+
const f = fetchFn || globalThis.fetch;
|
|
134
|
+
const authHeaders = apiKey.startsWith("tes_") ? { Authorization: `Bearer ${apiKey}` } : { "x-service-key": apiKey };
|
|
135
|
+
const response = await f(`${endpoint}/api/graphql`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: {
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
"x-client-id": clientId,
|
|
140
|
+
...headers,
|
|
141
|
+
...authHeaders
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
query: EMIT_EVENT_MUTATION,
|
|
145
|
+
variables: {
|
|
146
|
+
input: userId ? { ...input, data: { ...input.data, attributes: { ...input.data?.attributes, userId } } } : input
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
});
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
throw new Error(`TES API error: ${response.status}`);
|
|
152
|
+
}
|
|
153
|
+
const json = await response.json();
|
|
154
|
+
if (json.errors?.length) {
|
|
155
|
+
throw new Error(`TES GraphQL error: ${json.errors[0].message}`);
|
|
156
|
+
}
|
|
157
|
+
return json.data.emitEvent;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/tracking.js
|
|
161
|
+
var encoder = new TextEncoder();
|
|
162
|
+
function toBase64Url(buffer) {
|
|
163
|
+
const bytes = new Uint8Array(buffer);
|
|
164
|
+
let binary = "";
|
|
165
|
+
for (let i = 0; i < bytes.length; i++)
|
|
166
|
+
binary += String.fromCharCode(bytes[i]);
|
|
167
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
168
|
+
}
|
|
169
|
+
async function signPayload(secret, payload) {
|
|
170
|
+
const key = await crypto.subtle.importKey(
|
|
171
|
+
"raw",
|
|
172
|
+
encoder.encode(secret),
|
|
173
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
174
|
+
false,
|
|
175
|
+
["sign"]
|
|
176
|
+
);
|
|
177
|
+
const data = encoder.encode(JSON.stringify(payload));
|
|
178
|
+
const sig = await crypto.subtle.sign("HMAC", key, data);
|
|
179
|
+
return toBase64Url(sig);
|
|
180
|
+
}
|
|
181
|
+
async function verifyPayload(secret, payload, signature) {
|
|
182
|
+
const expected = await signPayload(secret, payload);
|
|
183
|
+
return expected === signature;
|
|
184
|
+
}
|
|
185
|
+
async function buildTrackUrl(endpoint, apiKey, payload) {
|
|
186
|
+
const p = { ...payload };
|
|
187
|
+
if (!p.e)
|
|
188
|
+
p.e = "LINK_CLICK";
|
|
189
|
+
const encoded = toBase64Url(encoder.encode(JSON.stringify(p)));
|
|
190
|
+
const sig = await signPayload(apiKey, p);
|
|
191
|
+
return `${endpoint}/r/${encoded}?sig=${sig}`;
|
|
192
|
+
}
|
|
193
|
+
var URL_RE = /https?:\/\/[^\s"'<>)\]]+/g;
|
|
194
|
+
async function rewriteUrls(text, config, sessionId, metadata) {
|
|
195
|
+
if (!text)
|
|
196
|
+
return text;
|
|
197
|
+
const redirectPrefix = `${config.endpoint}/r/`;
|
|
198
|
+
const matches = [...text.matchAll(URL_RE)];
|
|
199
|
+
if (matches.length === 0)
|
|
200
|
+
return text;
|
|
201
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
202
|
+
for (const m of matches) {
|
|
203
|
+
const originalUrl = m[0];
|
|
204
|
+
if (originalUrl.startsWith(redirectPrefix))
|
|
205
|
+
continue;
|
|
206
|
+
if (replacements.has(originalUrl))
|
|
207
|
+
continue;
|
|
208
|
+
const payload = {
|
|
209
|
+
u: originalUrl,
|
|
210
|
+
s: sessionId,
|
|
211
|
+
c: config.clientId,
|
|
212
|
+
t: Math.floor(Date.now() / 1e3)
|
|
213
|
+
};
|
|
214
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
215
|
+
payload.a = metadata;
|
|
216
|
+
}
|
|
217
|
+
const trackUrl = await buildTrackUrl(config.endpoint, config.apiKey, payload);
|
|
218
|
+
replacements.set(originalUrl, trackUrl);
|
|
219
|
+
}
|
|
220
|
+
let result = text;
|
|
221
|
+
const sorted = [...replacements.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
222
|
+
for (const [original, tracked] of sorted) {
|
|
223
|
+
result = result.split(original).join(tracked);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/session.js
|
|
229
|
+
function truncate(value, maxLen) {
|
|
230
|
+
if (!value || !maxLen || typeof value !== "string")
|
|
231
|
+
return value;
|
|
232
|
+
if (value.length <= maxLen)
|
|
233
|
+
return value;
|
|
234
|
+
return value.slice(0, maxLen) + "...[truncated]";
|
|
235
|
+
}
|
|
236
|
+
var Session = class {
|
|
237
|
+
constructor(clientConfig, { sessionId, metadata } = {}) {
|
|
238
|
+
Object.defineProperty(this, "_config", {
|
|
239
|
+
value: clientConfig,
|
|
240
|
+
enumerable: false
|
|
241
|
+
});
|
|
242
|
+
this.sessionId = sessionId || crypto.randomUUID();
|
|
243
|
+
this._metadata = metadata || {};
|
|
244
|
+
this._reset();
|
|
245
|
+
}
|
|
246
|
+
_reset() {
|
|
247
|
+
this._promptTokens = 0;
|
|
248
|
+
this._completionTokens = 0;
|
|
249
|
+
this._rounds = 0;
|
|
250
|
+
this._toolCalls = [];
|
|
251
|
+
this._model = null;
|
|
252
|
+
this._systemPrompt = null;
|
|
253
|
+
}
|
|
254
|
+
get totalUsage() {
|
|
255
|
+
return {
|
|
256
|
+
prompt_tokens: this._promptTokens,
|
|
257
|
+
completion_tokens: this._completionTokens,
|
|
258
|
+
total_tokens: this._promptTokens + this._completionTokens,
|
|
259
|
+
ai_rounds: this._rounds
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
get toolCalls() {
|
|
263
|
+
return this._toolCalls;
|
|
264
|
+
}
|
|
265
|
+
record(rawResponse) {
|
|
266
|
+
const normalized = normalizeResponse(rawResponse);
|
|
267
|
+
const round = this._rounds;
|
|
268
|
+
this._promptTokens += normalized.usage.prompt_tokens;
|
|
269
|
+
this._completionTokens += normalized.usage.completion_tokens;
|
|
270
|
+
this._rounds += 1;
|
|
271
|
+
if (normalized.model) {
|
|
272
|
+
this._model = normalized.model;
|
|
273
|
+
}
|
|
274
|
+
for (const tc of normalized.toolCalls) {
|
|
275
|
+
this._toolCalls.push({ ...tc, round });
|
|
276
|
+
}
|
|
277
|
+
return normalized;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Attach a result summary to the most recent tool call matching `toolName`.
|
|
281
|
+
* Call this after executing a tool to include results in the emitted event.
|
|
282
|
+
*/
|
|
283
|
+
recordToolResult(toolName, result) {
|
|
284
|
+
for (let i = this._toolCalls.length - 1; i >= 0; i--) {
|
|
285
|
+
if (this._toolCalls[i].tool === toolName && !this._toolCalls[i].result) {
|
|
286
|
+
this._toolCalls[i].result = result;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async emitChatTurn({ userMessage, assistantResponse, turnNumber, messages }) {
|
|
292
|
+
const capture = this._config.captureContent !== false;
|
|
293
|
+
const maxLen = this._config.maxContentLength;
|
|
294
|
+
const attributes = {
|
|
295
|
+
...this._metadata,
|
|
296
|
+
source: "pentatonic-ai-sdk",
|
|
297
|
+
model: this._model,
|
|
298
|
+
usage: this.totalUsage,
|
|
299
|
+
tool_calls: this._toolCalls.length ? capture ? this._toolCalls : this._toolCalls.map(({ args, ...rest }) => rest) : void 0
|
|
300
|
+
};
|
|
301
|
+
if (capture) {
|
|
302
|
+
attributes.user_message = truncate(userMessage, maxLen);
|
|
303
|
+
attributes.assistant_response = truncate(assistantResponse, maxLen);
|
|
304
|
+
if (this._systemPrompt) {
|
|
305
|
+
attributes.system_prompt = truncate(this._systemPrompt, maxLen);
|
|
306
|
+
}
|
|
307
|
+
if (messages) {
|
|
308
|
+
attributes.messages = messages.map((m) => {
|
|
309
|
+
if (typeof m.content === "string") {
|
|
310
|
+
return { ...m, content: truncate(m.content, maxLen) };
|
|
311
|
+
}
|
|
312
|
+
return m;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (turnNumber !== void 0) {
|
|
317
|
+
attributes.turn_number = turnNumber;
|
|
318
|
+
}
|
|
319
|
+
const result = await sendEvent(this._config, {
|
|
320
|
+
eventType: "CHAT_TURN",
|
|
321
|
+
entityType: "conversation",
|
|
322
|
+
data: {
|
|
323
|
+
entity_id: this.sessionId,
|
|
324
|
+
attributes
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
this._reset();
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
async emitToolUse({ tool, args, resultSummary, durationMs, turnNumber }) {
|
|
331
|
+
const capture = this._config.captureContent !== false;
|
|
332
|
+
const maxLen = this._config.maxContentLength;
|
|
333
|
+
const attributes = {
|
|
334
|
+
...this._metadata,
|
|
335
|
+
source: "pentatonic-ai-sdk",
|
|
336
|
+
tool,
|
|
337
|
+
duration_ms: durationMs,
|
|
338
|
+
turn_number: turnNumber
|
|
339
|
+
};
|
|
340
|
+
if (capture) {
|
|
341
|
+
attributes.args = args;
|
|
342
|
+
attributes.result_summary = truncate(resultSummary, maxLen);
|
|
343
|
+
}
|
|
344
|
+
return sendEvent(this._config, {
|
|
345
|
+
eventType: "TOOL_USE",
|
|
346
|
+
entityType: "conversation",
|
|
347
|
+
data: {
|
|
348
|
+
entity_id: this.sessionId,
|
|
349
|
+
attributes
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async trackUrl(url, { eventType, attributes } = {}) {
|
|
354
|
+
const payload = {
|
|
355
|
+
u: url,
|
|
356
|
+
s: this.sessionId,
|
|
357
|
+
c: this._config.clientId,
|
|
358
|
+
t: Math.floor(Date.now() / 1e3),
|
|
359
|
+
e: eventType || "LINK_CLICK"
|
|
360
|
+
};
|
|
361
|
+
const meta = { ...this._metadata, ...attributes };
|
|
362
|
+
if (Object.keys(meta).length) {
|
|
363
|
+
payload.a = meta;
|
|
364
|
+
}
|
|
365
|
+
return buildTrackUrl(this._config.endpoint, this._config.apiKey, payload);
|
|
366
|
+
}
|
|
367
|
+
async emitSessionStart() {
|
|
368
|
+
return sendEvent(this._config, {
|
|
369
|
+
eventType: "SESSION_START",
|
|
370
|
+
entityType: "conversation",
|
|
371
|
+
data: {
|
|
372
|
+
entity_id: this.sessionId,
|
|
373
|
+
attributes: {
|
|
374
|
+
source: "pentatonic-ai-sdk",
|
|
375
|
+
metadata: this._metadata
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// src/wrapper.js
|
|
383
|
+
function detectClientType(client) {
|
|
384
|
+
if (client?.chat?.completions?.create)
|
|
385
|
+
return "openai";
|
|
386
|
+
if (client?.messages?.create)
|
|
387
|
+
return "anthropic";
|
|
388
|
+
if (typeof client?.run === "function")
|
|
389
|
+
return "workers-ai";
|
|
390
|
+
return "unknown";
|
|
391
|
+
}
|
|
392
|
+
function wrapClient(clientConfig, client, sessionOpts = {}) {
|
|
393
|
+
sessionOpts._resolvedSessionId = sessionOpts.sessionId || crypto.randomUUID();
|
|
394
|
+
sessionOpts._session = new Session(clientConfig, {
|
|
395
|
+
sessionId: sessionOpts._resolvedSessionId,
|
|
396
|
+
metadata: sessionOpts.metadata
|
|
397
|
+
});
|
|
398
|
+
const type = detectClientType(client);
|
|
399
|
+
if (type === "openai")
|
|
400
|
+
return wrapOpenAI(clientConfig, client, sessionOpts);
|
|
401
|
+
if (type === "anthropic")
|
|
402
|
+
return wrapAnthropic(clientConfig, client, sessionOpts);
|
|
403
|
+
if (type === "workers-ai")
|
|
404
|
+
return wrapWorkersAI(clientConfig, client, sessionOpts);
|
|
405
|
+
throw new Error(
|
|
406
|
+
"Unsupported client: expected OpenAI (chat.completions.create), Anthropic (messages.create), or Workers AI (run) client"
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
function wrapOpenAI(clientConfig, client, sessionOpts) {
|
|
410
|
+
return new Proxy(client, {
|
|
411
|
+
get(target, prop) {
|
|
412
|
+
if (prop === "chat")
|
|
413
|
+
return wrapOpenAIChat(clientConfig, target.chat, target, sessionOpts);
|
|
414
|
+
if (prop === "sessionId")
|
|
415
|
+
return sessionOpts._resolvedSessionId;
|
|
416
|
+
if (prop === "tesSession")
|
|
417
|
+
return sessionOpts._session;
|
|
418
|
+
if (prop === "session")
|
|
419
|
+
return (opts) => new OpenAISession(clientConfig, target, opts);
|
|
420
|
+
return target[prop];
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function wrapOpenAIChat(clientConfig, chat, client, sessionOpts) {
|
|
425
|
+
return new Proxy(chat, {
|
|
426
|
+
get(target, prop) {
|
|
427
|
+
if (prop === "completions")
|
|
428
|
+
return wrapOpenAICompletions(
|
|
429
|
+
clientConfig,
|
|
430
|
+
target.completions,
|
|
431
|
+
client,
|
|
432
|
+
sessionOpts
|
|
433
|
+
);
|
|
434
|
+
return target[prop];
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
function wrapOpenAICompletions(clientConfig, completions, client, sessionOpts) {
|
|
439
|
+
return new Proxy(completions, {
|
|
440
|
+
get(target, prop) {
|
|
441
|
+
if (prop === "create") {
|
|
442
|
+
return async (params) => {
|
|
443
|
+
const result = await target.create(params);
|
|
444
|
+
const content = result.choices?.[0]?.message?.content;
|
|
445
|
+
if (content) {
|
|
446
|
+
result.choices[0].message.content = await rewriteUrls(
|
|
447
|
+
content,
|
|
448
|
+
clientConfig,
|
|
449
|
+
sessionOpts._resolvedSessionId,
|
|
450
|
+
sessionOpts.metadata
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
fireAndForgetEmit(
|
|
454
|
+
clientConfig,
|
|
455
|
+
sessionOpts,
|
|
456
|
+
params.messages,
|
|
457
|
+
result
|
|
458
|
+
);
|
|
459
|
+
return result;
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
return target[prop];
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
var OpenAISession = class extends Session {
|
|
467
|
+
constructor(clientConfig, client, opts) {
|
|
468
|
+
super(clientConfig, opts);
|
|
469
|
+
this._client = client;
|
|
470
|
+
}
|
|
471
|
+
async chat(params) {
|
|
472
|
+
const result = await this._client.chat.completions.create(params);
|
|
473
|
+
this.record(result);
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
function wrapAnthropic(clientConfig, client, sessionOpts) {
|
|
478
|
+
return new Proxy(client, {
|
|
479
|
+
get(target, prop) {
|
|
480
|
+
if (prop === "messages")
|
|
481
|
+
return wrapAnthropicMessages(
|
|
482
|
+
clientConfig,
|
|
483
|
+
target.messages,
|
|
484
|
+
target,
|
|
485
|
+
sessionOpts
|
|
486
|
+
);
|
|
487
|
+
if (prop === "sessionId")
|
|
488
|
+
return sessionOpts._resolvedSessionId;
|
|
489
|
+
if (prop === "tesSession")
|
|
490
|
+
return sessionOpts._session;
|
|
491
|
+
if (prop === "session")
|
|
492
|
+
return (opts) => new AnthropicSession(clientConfig, target, opts);
|
|
493
|
+
return target[prop];
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
function wrapAnthropicMessages(clientConfig, messages, client, sessionOpts) {
|
|
498
|
+
return new Proxy(messages, {
|
|
499
|
+
get(target, prop) {
|
|
500
|
+
if (prop === "create") {
|
|
501
|
+
return async (params) => {
|
|
502
|
+
const result = await target.create(params);
|
|
503
|
+
if (Array.isArray(result.content)) {
|
|
504
|
+
for (const block of result.content) {
|
|
505
|
+
if (block.type === "text" && block.text) {
|
|
506
|
+
block.text = await rewriteUrls(
|
|
507
|
+
block.text,
|
|
508
|
+
clientConfig,
|
|
509
|
+
sessionOpts._resolvedSessionId,
|
|
510
|
+
sessionOpts.metadata
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
fireAndForgetEmit(
|
|
516
|
+
clientConfig,
|
|
517
|
+
sessionOpts,
|
|
518
|
+
params.messages,
|
|
519
|
+
result
|
|
520
|
+
);
|
|
521
|
+
return result;
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return target[prop];
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
var AnthropicSession = class extends Session {
|
|
529
|
+
constructor(clientConfig, client, opts) {
|
|
530
|
+
super(clientConfig, opts);
|
|
531
|
+
this._client = client;
|
|
532
|
+
}
|
|
533
|
+
async chat(params) {
|
|
534
|
+
const result = await this._client.messages.create(params);
|
|
535
|
+
this.record(result);
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
function wrapWorkersAI(clientConfig, aiBinding, sessionOpts) {
|
|
540
|
+
return new Proxy(aiBinding, {
|
|
541
|
+
get(target, prop) {
|
|
542
|
+
if (prop === "run") {
|
|
543
|
+
return async (model, params, ...rest) => {
|
|
544
|
+
const result = await target.run(model, params, ...rest);
|
|
545
|
+
if (result.response) {
|
|
546
|
+
result.response = await rewriteUrls(
|
|
547
|
+
result.response,
|
|
548
|
+
clientConfig,
|
|
549
|
+
sessionOpts._resolvedSessionId,
|
|
550
|
+
sessionOpts.metadata
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
fireAndForgetEmit(
|
|
554
|
+
clientConfig,
|
|
555
|
+
sessionOpts,
|
|
556
|
+
params?.messages,
|
|
557
|
+
result,
|
|
558
|
+
model
|
|
559
|
+
);
|
|
560
|
+
return result;
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (prop === "sessionId")
|
|
564
|
+
return sessionOpts._resolvedSessionId;
|
|
565
|
+
if (prop === "tesSession")
|
|
566
|
+
return sessionOpts._session;
|
|
567
|
+
if (prop === "session")
|
|
568
|
+
return (opts) => new WorkersAISession(clientConfig, target, opts);
|
|
569
|
+
return target[prop];
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
var WorkersAISession = class extends Session {
|
|
574
|
+
constructor(clientConfig, aiBinding, opts) {
|
|
575
|
+
super(clientConfig, opts);
|
|
576
|
+
this._ai = aiBinding;
|
|
577
|
+
}
|
|
578
|
+
async chat(model, params, ...rest) {
|
|
579
|
+
const result = await this._ai.run(model, params, ...rest);
|
|
580
|
+
this.record(result);
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
function extractToolResults(session, messages) {
|
|
585
|
+
if (!messages?.length || !session._toolCalls.length)
|
|
586
|
+
return;
|
|
587
|
+
const idToName = /* @__PURE__ */ new Map();
|
|
588
|
+
for (const msg of messages) {
|
|
589
|
+
if (msg.role === "assistant" && msg.tool_calls) {
|
|
590
|
+
for (const tc of msg.tool_calls) {
|
|
591
|
+
const id = tc.id || tc.tool_call_id;
|
|
592
|
+
const name = tc.function?.name || tc.name;
|
|
593
|
+
if (id && name)
|
|
594
|
+
idToName.set(id, name);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
for (const msg of messages) {
|
|
599
|
+
if (msg.role !== "tool" || !msg.content)
|
|
600
|
+
continue;
|
|
601
|
+
const callId = msg.tool_call_id;
|
|
602
|
+
const toolName = callId ? idToName.get(callId) : null;
|
|
603
|
+
for (const tc of session._toolCalls) {
|
|
604
|
+
if (tc.result)
|
|
605
|
+
continue;
|
|
606
|
+
if (toolName && tc.tool !== toolName)
|
|
607
|
+
continue;
|
|
608
|
+
try {
|
|
609
|
+
const parsed = JSON.parse(msg.content);
|
|
610
|
+
if (Array.isArray(parsed)) {
|
|
611
|
+
tc.result = { count: parsed.length, sample: parsed.slice(0, 3) };
|
|
612
|
+
} else {
|
|
613
|
+
tc.result = parsed;
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
tc.result = msg.content;
|
|
617
|
+
}
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function fireAndForgetEmit(clientConfig, sessionOpts, messages, result, model) {
|
|
623
|
+
const session = sessionOpts._session;
|
|
624
|
+
const normalized = session.record(result);
|
|
625
|
+
extractToolResults(session, messages);
|
|
626
|
+
if (!session._systemPrompt && messages?.length) {
|
|
627
|
+
const systemMsg = messages.find((m) => m.role === "system");
|
|
628
|
+
if (systemMsg?.content) {
|
|
629
|
+
session._systemPrompt = systemMsg.content;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (model && !normalized.model) {
|
|
633
|
+
session._model = model;
|
|
634
|
+
}
|
|
635
|
+
if (sessionOpts.autoEmit === false) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (!normalized.content && normalized.toolCalls.length > 0) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const userMsg = messages?.filter?.((m) => m.role === "user")?.pop()?.content || "";
|
|
642
|
+
const assistantMsg = normalized.content || "";
|
|
643
|
+
const emitPromise = session.emitChatTurn({ userMessage: userMsg, assistantResponse: assistantMsg }).catch((err) => console.error("[pentatonic-ai] emit failed:", err.message));
|
|
644
|
+
if (typeof sessionOpts.waitUntil === "function") {
|
|
645
|
+
sessionOpts.waitUntil(emitPromise);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/client.js
|
|
650
|
+
var TESClient = class {
|
|
651
|
+
constructor({ clientId, apiKey, endpoint, headers, userId, captureContent = true, maxContentLength = 4096 }) {
|
|
652
|
+
if (!clientId)
|
|
653
|
+
throw new Error("clientId is required");
|
|
654
|
+
if (!apiKey)
|
|
655
|
+
throw new Error("apiKey is required");
|
|
656
|
+
if (!endpoint)
|
|
657
|
+
throw new Error("endpoint is required");
|
|
658
|
+
const cleanEndpoint = endpoint.replace(/\/$/, "");
|
|
659
|
+
const isLocalDev = /^http:\/\/localhost(:\d+)?(\/|$)/.test(cleanEndpoint) || /^http:\/\/127\.0\.0\.1(:\d+)?(\/|$)/.test(cleanEndpoint);
|
|
660
|
+
if (!cleanEndpoint.startsWith("https://") && !isLocalDev) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
"endpoint must use https:// (http:// is only allowed for localhost)"
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
this.clientId = clientId;
|
|
666
|
+
this.endpoint = cleanEndpoint;
|
|
667
|
+
this.userId = userId || null;
|
|
668
|
+
this.captureContent = captureContent;
|
|
669
|
+
this.maxContentLength = maxContentLength;
|
|
670
|
+
Object.defineProperty(this, "_apiKey", {
|
|
671
|
+
value: apiKey,
|
|
672
|
+
enumerable: false,
|
|
673
|
+
writable: false
|
|
674
|
+
});
|
|
675
|
+
Object.defineProperty(this, "_headers", {
|
|
676
|
+
value: headers || {},
|
|
677
|
+
enumerable: false,
|
|
678
|
+
writable: false
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
get _config() {
|
|
682
|
+
return {
|
|
683
|
+
clientId: this.clientId,
|
|
684
|
+
apiKey: this._apiKey,
|
|
685
|
+
endpoint: this.endpoint,
|
|
686
|
+
headers: this._headers,
|
|
687
|
+
userId: this.userId,
|
|
688
|
+
captureContent: this.captureContent,
|
|
689
|
+
maxContentLength: this.maxContentLength
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
session(opts) {
|
|
693
|
+
return new Session(this._config, opts);
|
|
694
|
+
}
|
|
695
|
+
wrap(client, { sessionId, userId, metadata, autoEmit = true, waitUntil } = {}) {
|
|
696
|
+
const config = userId ? { ...this._config, userId } : this._config;
|
|
697
|
+
return wrapClient(config, client, { sessionId, metadata, autoEmit, waitUntil });
|
|
698
|
+
}
|
|
699
|
+
};
|