@sensu-ai/sdk 0.1.5 → 0.5.0
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/dist/client.d.ts +128 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +837 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/langchain.d.ts +60 -0
- package/dist/integrations/langchain.d.ts.map +1 -0
- package/dist/integrations/langchain.js +238 -0
- package/dist/integrations/langchain.js.map +1 -0
- package/dist/integrations/openai.d.ts +25 -0
- package/dist/integrations/openai.d.ts.map +1 -0
- package/dist/integrations/openai.js +69 -0
- package/dist/integrations/openai.js.map +1 -0
- package/dist/types.d.ts +224 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +20 -6
- package/src/client.ts +0 -771
- package/src/index.ts +0 -24
- package/src/integrations/langchain.ts +0 -175
- package/src/integrations/openai.ts +0 -109
- package/src/types.ts +0 -213
- package/tsconfig.json +0 -11
package/dist/client.js
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// StepHandle — fluent API for a single step
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export class StepHandle {
|
|
6
|
+
client;
|
|
7
|
+
stepId;
|
|
8
|
+
runId;
|
|
9
|
+
sessionId;
|
|
10
|
+
agentId;
|
|
11
|
+
orgId;
|
|
12
|
+
traceId;
|
|
13
|
+
spanId;
|
|
14
|
+
sequence;
|
|
15
|
+
stepName;
|
|
16
|
+
ended = false;
|
|
17
|
+
constructor(client, opts) {
|
|
18
|
+
this.client = client;
|
|
19
|
+
this.stepId = opts.stepId;
|
|
20
|
+
this.runId = opts.runId;
|
|
21
|
+
this.sessionId = opts.sessionId;
|
|
22
|
+
this.agentId = opts.agentId;
|
|
23
|
+
this.orgId = opts.orgId;
|
|
24
|
+
this.traceId = opts.traceId;
|
|
25
|
+
this.spanId = opts.spanId;
|
|
26
|
+
this.sequence = opts.sequence;
|
|
27
|
+
this.stepName = opts.name;
|
|
28
|
+
}
|
|
29
|
+
base() {
|
|
30
|
+
return {
|
|
31
|
+
event_id: randomUUID(),
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
org_id: this.orgId,
|
|
34
|
+
agent_id: this.agentId,
|
|
35
|
+
session_id: this.sessionId,
|
|
36
|
+
run_id: this.runId,
|
|
37
|
+
step_id: this.stepId,
|
|
38
|
+
trace_id: this.traceId,
|
|
39
|
+
span_id: randomUUID(),
|
|
40
|
+
parent_span_id: this.spanId,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Track an LLM call — wraps fn(), measures latency, emits event */
|
|
44
|
+
async trackLlm(opts) {
|
|
45
|
+
const startMs = Date.now();
|
|
46
|
+
const spanId = randomUUID();
|
|
47
|
+
const llmCallId = opts.llmCallId ?? randomUUID();
|
|
48
|
+
this.client.enqueue({
|
|
49
|
+
...this.base(),
|
|
50
|
+
span_id: spanId,
|
|
51
|
+
event_type: 'llm.request.started',
|
|
52
|
+
provider: opts.provider,
|
|
53
|
+
model: opts.model,
|
|
54
|
+
max_context_tokens: opts.maxContextTokens,
|
|
55
|
+
});
|
|
56
|
+
let result;
|
|
57
|
+
let status = 'success';
|
|
58
|
+
let err;
|
|
59
|
+
try {
|
|
60
|
+
result = await opts.fn();
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
status = 'error';
|
|
64
|
+
err = e;
|
|
65
|
+
}
|
|
66
|
+
const latencyMs = Date.now() - startMs;
|
|
67
|
+
// Try to extract token usage from common response shapes
|
|
68
|
+
const usage = extractUsage(result, opts.model);
|
|
69
|
+
const contextBreakdown = opts.extractContextBreakdown?.(result);
|
|
70
|
+
// Override cost estimate with live pricing when tokens are known
|
|
71
|
+
const inputTok = usage['input_tokens'] ?? 0;
|
|
72
|
+
const outputTok = usage['output_tokens'] ?? 0;
|
|
73
|
+
if (inputTok > 0 || outputTok > 0) {
|
|
74
|
+
try {
|
|
75
|
+
const [inputRate, outputRate] = await this.client.resolvePricing(opts.provider, opts.model);
|
|
76
|
+
usage['cost_usd_estimate'] =
|
|
77
|
+
(inputTok / 1_000_000) * inputRate + (outputTok / 1_000_000) * outputRate;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// keep bundled estimate on failure
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this.client.enqueue({
|
|
84
|
+
...this.base(),
|
|
85
|
+
span_id: spanId,
|
|
86
|
+
event_type: 'llm.request.completed',
|
|
87
|
+
llm_call_id: llmCallId,
|
|
88
|
+
provider: opts.provider,
|
|
89
|
+
model: opts.model,
|
|
90
|
+
max_context_tokens: opts.maxContextTokens,
|
|
91
|
+
latency_ms: latencyMs,
|
|
92
|
+
status,
|
|
93
|
+
...usage,
|
|
94
|
+
...(contextBreakdown ? { context_breakdown: contextBreakdown } : {}),
|
|
95
|
+
...(opts.messagesSnapshot?.length ? { messages_snapshot: opts.messagesSnapshot } : {}),
|
|
96
|
+
...(opts.referencedChunkIds?.length ? { referenced_chunk_ids: opts.referencedChunkIds } : {}),
|
|
97
|
+
});
|
|
98
|
+
if (err)
|
|
99
|
+
throw err;
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Track a streaming LLM call — consumes the async iterable, measures TTFT,
|
|
104
|
+
* and emits stream.token.received events as tokens arrive.
|
|
105
|
+
* Returns the full concatenated text.
|
|
106
|
+
*/
|
|
107
|
+
async trackStreamingLlm(opts) {
|
|
108
|
+
const startMs = Date.now();
|
|
109
|
+
const llmCallId = opts.llmCallId ?? randomUUID();
|
|
110
|
+
const emitEvery = opts.emitEveryNTokens ?? 10;
|
|
111
|
+
const spanId = randomUUID();
|
|
112
|
+
this.client.enqueue({
|
|
113
|
+
...this.base(),
|
|
114
|
+
span_id: spanId,
|
|
115
|
+
event_type: 'llm.request.started',
|
|
116
|
+
provider: opts.provider,
|
|
117
|
+
model: opts.model,
|
|
118
|
+
max_context_tokens: opts.maxContextTokens,
|
|
119
|
+
stream: true,
|
|
120
|
+
});
|
|
121
|
+
let ttftMs;
|
|
122
|
+
let tokenCount = 0;
|
|
123
|
+
let accumulated = '';
|
|
124
|
+
for await (const chunk of opts.stream) {
|
|
125
|
+
// Capture time-to-first-token on the very first chunk
|
|
126
|
+
if (ttftMs === undefined) {
|
|
127
|
+
ttftMs = Date.now() - startMs;
|
|
128
|
+
}
|
|
129
|
+
// Extract text from common chunk shapes (Anthropic / OpenAI streaming)
|
|
130
|
+
const text = extractStreamChunkText(chunk);
|
|
131
|
+
if (text) {
|
|
132
|
+
accumulated += text;
|
|
133
|
+
tokenCount++;
|
|
134
|
+
}
|
|
135
|
+
// Emit stream.token.received every N tokens
|
|
136
|
+
if (tokenCount > 0 && tokenCount % emitEvery === 0) {
|
|
137
|
+
this.client.enqueue({
|
|
138
|
+
...this.base(),
|
|
139
|
+
event_type: 'stream.token.received',
|
|
140
|
+
llm_call_id: llmCallId,
|
|
141
|
+
tokens_so_far: tokenCount,
|
|
142
|
+
ttft_ms: ttftMs,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const latencyMs = Date.now() - startMs;
|
|
147
|
+
this.client.enqueue({
|
|
148
|
+
...this.base(),
|
|
149
|
+
span_id: spanId,
|
|
150
|
+
event_type: 'llm.request.completed',
|
|
151
|
+
llm_call_id: llmCallId,
|
|
152
|
+
provider: opts.provider,
|
|
153
|
+
model: opts.model,
|
|
154
|
+
max_context_tokens: opts.maxContextTokens,
|
|
155
|
+
latency_ms: latencyMs,
|
|
156
|
+
ttft_ms: ttftMs,
|
|
157
|
+
streamed: true,
|
|
158
|
+
status: 'success',
|
|
159
|
+
});
|
|
160
|
+
opts.onComplete?.(accumulated, ttftMs);
|
|
161
|
+
return accumulated;
|
|
162
|
+
}
|
|
163
|
+
/** Emit a raw LLM call event (when you have the stats already) */
|
|
164
|
+
recordLlm(opts) {
|
|
165
|
+
const { contextBreakdown, ...rest } = opts;
|
|
166
|
+
this.client.enqueue({
|
|
167
|
+
...this.base(),
|
|
168
|
+
event_type: 'llm.request.completed',
|
|
169
|
+
...rest,
|
|
170
|
+
...(contextBreakdown ? { context_breakdown: contextBreakdown } : {}),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Track a tool call — wraps fn(), measures latency */
|
|
174
|
+
async trackTool(opts) {
|
|
175
|
+
const startMs = Date.now();
|
|
176
|
+
const toolCallId = randomUUID();
|
|
177
|
+
let result;
|
|
178
|
+
let status = 'success';
|
|
179
|
+
let err;
|
|
180
|
+
this.client.enqueue({
|
|
181
|
+
...this.base(),
|
|
182
|
+
event_type: 'tool.call.started',
|
|
183
|
+
tool_name: opts.toolName,
|
|
184
|
+
tool_call_id: toolCallId,
|
|
185
|
+
retry_of: opts.retryOf,
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
result = await opts.fn();
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
status = 'error';
|
|
192
|
+
err = e;
|
|
193
|
+
}
|
|
194
|
+
const latencyMs = Date.now() - startMs;
|
|
195
|
+
const outputSize = estimateBytes(result);
|
|
196
|
+
this.client.enqueue({
|
|
197
|
+
...this.base(),
|
|
198
|
+
event_type: 'tool.call.completed',
|
|
199
|
+
tool_name: opts.toolName,
|
|
200
|
+
latency_ms: latencyMs,
|
|
201
|
+
status,
|
|
202
|
+
output_size_bytes: outputSize,
|
|
203
|
+
tool_call_id: toolCallId,
|
|
204
|
+
retry_of: opts.retryOf,
|
|
205
|
+
});
|
|
206
|
+
this.client.notifyToolCall(this.runId, opts.toolName);
|
|
207
|
+
if (err)
|
|
208
|
+
throw err;
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
/** Track a retrieval call — wraps fn(), measures latency, emits started + completed */
|
|
212
|
+
async trackRetrieval(opts) {
|
|
213
|
+
const startMs = Date.now();
|
|
214
|
+
const spanId = randomUUID();
|
|
215
|
+
this.client.enqueue({
|
|
216
|
+
...this.base(),
|
|
217
|
+
span_id: spanId,
|
|
218
|
+
event_type: 'retrieval.started',
|
|
219
|
+
vector_store_id: opts.vectorStoreId,
|
|
220
|
+
top_k: opts.topK,
|
|
221
|
+
});
|
|
222
|
+
let result;
|
|
223
|
+
let status = 'success';
|
|
224
|
+
let err;
|
|
225
|
+
try {
|
|
226
|
+
result = await opts.fn();
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
status = 'error';
|
|
230
|
+
err = e;
|
|
231
|
+
}
|
|
232
|
+
const latencyMs = Date.now() - startMs;
|
|
233
|
+
this.client.enqueue({
|
|
234
|
+
...this.base(),
|
|
235
|
+
span_id: spanId,
|
|
236
|
+
event_type: 'retrieval.completed',
|
|
237
|
+
vector_store_id: opts.vectorStoreId,
|
|
238
|
+
top_k: opts.topK,
|
|
239
|
+
latency_ms: latencyMs,
|
|
240
|
+
status,
|
|
241
|
+
});
|
|
242
|
+
if (err)
|
|
243
|
+
throw err;
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
/** Emit a raw retrieval completed event (when you have the stats already) */
|
|
247
|
+
recordRetrieval(opts) {
|
|
248
|
+
const chunks = opts.chunks;
|
|
249
|
+
this.client.enqueue({
|
|
250
|
+
...this.base(),
|
|
251
|
+
event_type: 'retrieval.completed',
|
|
252
|
+
vector_store_id: opts.vectorStoreId,
|
|
253
|
+
top_k: opts.topK,
|
|
254
|
+
latency_ms: opts.latencyMs,
|
|
255
|
+
chunks_returned: opts.chunksReturned,
|
|
256
|
+
tokens_injected: opts.tokensInjected,
|
|
257
|
+
similarity_score_avg: opts.similarityScoreAvg,
|
|
258
|
+
status: opts.status,
|
|
259
|
+
...(chunks?.length ? { chunks } : {}),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/** Track an embedding call — wraps fn(), measures latency */
|
|
263
|
+
async trackEmbedding(opts) {
|
|
264
|
+
const startMs = Date.now();
|
|
265
|
+
let result;
|
|
266
|
+
let err;
|
|
267
|
+
try {
|
|
268
|
+
result = await opts.fn();
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
err = e;
|
|
272
|
+
}
|
|
273
|
+
const latencyMs = Date.now() - startMs;
|
|
274
|
+
this.client.enqueue({
|
|
275
|
+
...this.base(),
|
|
276
|
+
event_type: 'embedding.created',
|
|
277
|
+
model: opts.model,
|
|
278
|
+
input_text_length: opts.inputTextLength,
|
|
279
|
+
batch_size: opts.batchSize,
|
|
280
|
+
latency_ms: latencyMs,
|
|
281
|
+
});
|
|
282
|
+
if (err)
|
|
283
|
+
throw err;
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
/** Emit a raw embedding event */
|
|
287
|
+
recordEmbedding(opts) {
|
|
288
|
+
this.client.enqueue({
|
|
289
|
+
...this.base(),
|
|
290
|
+
event_type: 'embedding.created',
|
|
291
|
+
model: opts.model,
|
|
292
|
+
input_text_length: opts.inputTextLength,
|
|
293
|
+
token_count: opts.tokenCount,
|
|
294
|
+
latency_ms: opts.latencyMs,
|
|
295
|
+
cost_usd_estimate: opts.costUsdEstimate,
|
|
296
|
+
batch_size: opts.batchSize,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/** Track a guardrail check — wraps fn(), measures latency, handles block */
|
|
300
|
+
async trackGuardrail(opts) {
|
|
301
|
+
const startMs = Date.now();
|
|
302
|
+
this.client.enqueue({
|
|
303
|
+
...this.base(),
|
|
304
|
+
event_type: 'guardrail.check.started',
|
|
305
|
+
guardrail_id: opts.guardrailId,
|
|
306
|
+
guardrail_type: opts.guardrailType,
|
|
307
|
+
input_hash: opts.inputHash,
|
|
308
|
+
});
|
|
309
|
+
let result = 'pass';
|
|
310
|
+
let err;
|
|
311
|
+
try {
|
|
312
|
+
result = await opts.fn();
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
err = e;
|
|
316
|
+
}
|
|
317
|
+
const latencyMs = Date.now() - startMs;
|
|
318
|
+
this.client.enqueue({
|
|
319
|
+
...this.base(),
|
|
320
|
+
event_type: 'guardrail.check.completed',
|
|
321
|
+
guardrail_id: opts.guardrailId,
|
|
322
|
+
guardrail_type: opts.guardrailType,
|
|
323
|
+
input_hash: opts.inputHash,
|
|
324
|
+
result,
|
|
325
|
+
latency_ms: latencyMs,
|
|
326
|
+
});
|
|
327
|
+
if (err)
|
|
328
|
+
throw err;
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
/** Emit a raw guardrail result (check or block) */
|
|
332
|
+
recordGuardrail(opts) {
|
|
333
|
+
if (opts.blocked) {
|
|
334
|
+
this.client.enqueue({
|
|
335
|
+
...this.base(),
|
|
336
|
+
event_type: 'guardrail.blocked',
|
|
337
|
+
guardrail_id: opts.guardrailId,
|
|
338
|
+
guardrail_type: opts.guardrailType,
|
|
339
|
+
input_hash: opts.inputHash,
|
|
340
|
+
block_reason: opts.blockReason,
|
|
341
|
+
severity: opts.severity,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
this.client.enqueue({
|
|
346
|
+
...this.base(),
|
|
347
|
+
event_type: 'guardrail.check.completed',
|
|
348
|
+
guardrail_id: opts.guardrailId,
|
|
349
|
+
guardrail_type: opts.guardrailType,
|
|
350
|
+
input_hash: opts.inputHash,
|
|
351
|
+
result: opts.result,
|
|
352
|
+
latency_ms: opts.latencyMs,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/** Record a prompt template render event */
|
|
357
|
+
recordPromptRender(opts) {
|
|
358
|
+
this.client.enqueue({
|
|
359
|
+
...this.base(),
|
|
360
|
+
event_type: 'prompt.rendered',
|
|
361
|
+
template_id: opts.templateId,
|
|
362
|
+
template_version: opts.templateVersion,
|
|
363
|
+
rendered_token_count: opts.renderedTokenCount,
|
|
364
|
+
variable_count: opts.variableCount,
|
|
365
|
+
latency_ms: opts.latencyMs,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async end() {
|
|
369
|
+
if (this.ended)
|
|
370
|
+
return;
|
|
371
|
+
this.ended = true;
|
|
372
|
+
this.client.enqueue({
|
|
373
|
+
...this.base(),
|
|
374
|
+
event_type: 'agent.step.completed',
|
|
375
|
+
});
|
|
376
|
+
await this.client.flush();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// RunHandle — fluent API for a single run
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
export class RunHandle {
|
|
383
|
+
client;
|
|
384
|
+
runId;
|
|
385
|
+
sessionId;
|
|
386
|
+
agentId;
|
|
387
|
+
orgId;
|
|
388
|
+
traceId;
|
|
389
|
+
spanId;
|
|
390
|
+
stepCount = 0;
|
|
391
|
+
ended = false;
|
|
392
|
+
constructor(client, opts) {
|
|
393
|
+
this.client = client;
|
|
394
|
+
this.runId = opts.runId;
|
|
395
|
+
this.sessionId = opts.sessionId;
|
|
396
|
+
this.agentId = opts.agentId;
|
|
397
|
+
this.orgId = opts.orgId;
|
|
398
|
+
this.traceId = opts.traceId;
|
|
399
|
+
this.spanId = opts.spanId;
|
|
400
|
+
}
|
|
401
|
+
base() {
|
|
402
|
+
return {
|
|
403
|
+
event_id: randomUUID(),
|
|
404
|
+
timestamp: new Date().toISOString(),
|
|
405
|
+
org_id: this.orgId,
|
|
406
|
+
agent_id: this.agentId,
|
|
407
|
+
session_id: this.sessionId,
|
|
408
|
+
run_id: this.runId,
|
|
409
|
+
trace_id: this.traceId,
|
|
410
|
+
span_id: randomUUID(),
|
|
411
|
+
parent_span_id: this.spanId,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
startStep(opts = {}) {
|
|
415
|
+
const stepId = opts.stepId ?? randomUUID();
|
|
416
|
+
const sequence = this.stepCount++;
|
|
417
|
+
this.client.enqueue({
|
|
418
|
+
...this.base(),
|
|
419
|
+
step_id: stepId,
|
|
420
|
+
event_type: 'agent.step.started',
|
|
421
|
+
step_type: opts.stepType ?? 'llm',
|
|
422
|
+
step_name: opts.name,
|
|
423
|
+
sequence: opts.sequence ?? sequence,
|
|
424
|
+
});
|
|
425
|
+
return new StepHandle(this.client, {
|
|
426
|
+
stepId,
|
|
427
|
+
runId: this.runId,
|
|
428
|
+
sessionId: this.sessionId,
|
|
429
|
+
agentId: this.agentId,
|
|
430
|
+
orgId: this.orgId,
|
|
431
|
+
traceId: this.traceId,
|
|
432
|
+
spanId: this.spanId,
|
|
433
|
+
sequence: opts.sequence ?? sequence,
|
|
434
|
+
name: opts.name,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/** Record user feedback for this run */
|
|
438
|
+
recordFeedback(opts) {
|
|
439
|
+
this.client.enqueue({
|
|
440
|
+
...this.base(),
|
|
441
|
+
event_type: 'feedback.received',
|
|
442
|
+
type: opts.type,
|
|
443
|
+
score: opts.score,
|
|
444
|
+
comment: opts.comment,
|
|
445
|
+
end_user_id: opts.endUserId,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
/** Record an automated eval score for this run */
|
|
449
|
+
recordEvalScore(opts) {
|
|
450
|
+
this.client.enqueue({
|
|
451
|
+
...this.base(),
|
|
452
|
+
event_type: 'eval.score.recorded',
|
|
453
|
+
metric: opts.metric,
|
|
454
|
+
score: opts.score,
|
|
455
|
+
evaluator_id: opts.evaluatorId,
|
|
456
|
+
model_used_for_eval: opts.modelUsedForEval,
|
|
457
|
+
...(opts.stepId ? { step_id: opts.stepId } : {}),
|
|
458
|
+
...(opts.llmCallId ? { llm_call_id: opts.llmCallId } : {}),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/** Emit an agent.handoff event from this run to another agent */
|
|
462
|
+
handoff(opts) {
|
|
463
|
+
this.client.enqueue({
|
|
464
|
+
...this.base(),
|
|
465
|
+
event_type: 'agent.handoff',
|
|
466
|
+
to_agent_id: opts.toAgentId,
|
|
467
|
+
reason: opts.reason,
|
|
468
|
+
context_tokens_transferred: opts.contextTokensTransferred,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async end(status = 'completed') {
|
|
472
|
+
if (this.ended)
|
|
473
|
+
return;
|
|
474
|
+
this.ended = true;
|
|
475
|
+
const eventType = status === 'completed' ? 'agent.run.completed' : 'agent.run.failed';
|
|
476
|
+
this.client.enqueue({ ...this.base(), event_type: eventType });
|
|
477
|
+
this.client.clearRunLoopState(this.runId);
|
|
478
|
+
await this.client.flush();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// SensuClient
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
export class SensuClient {
|
|
485
|
+
apiKey;
|
|
486
|
+
baseUrl;
|
|
487
|
+
agentId;
|
|
488
|
+
orgId;
|
|
489
|
+
batchSize;
|
|
490
|
+
flushIntervalMs;
|
|
491
|
+
disabled;
|
|
492
|
+
disableLivePricing;
|
|
493
|
+
onLoopDetected;
|
|
494
|
+
loopThreshold;
|
|
495
|
+
// runId → toolName → call count within that run
|
|
496
|
+
runToolCallCounts = new Map();
|
|
497
|
+
// provider:model → [inputPricePer1M, outputPricePer1M]
|
|
498
|
+
pricingCache = new Map();
|
|
499
|
+
buffer = [];
|
|
500
|
+
flushTimer = null;
|
|
501
|
+
constructor(opts = {}) {
|
|
502
|
+
const fromEnv = opts.fromEnv ?? false;
|
|
503
|
+
this.apiKey =
|
|
504
|
+
opts.apiKey ??
|
|
505
|
+
(fromEnv ? (process.env.SENSU_API_KEY ?? '') : '');
|
|
506
|
+
this.baseUrl =
|
|
507
|
+
opts.baseUrl ??
|
|
508
|
+
(fromEnv
|
|
509
|
+
? (process.env.SENSU_BASE_URL ?? 'http://localhost:3001')
|
|
510
|
+
: 'http://localhost:3001');
|
|
511
|
+
this.agentId =
|
|
512
|
+
opts.agentId ??
|
|
513
|
+
(fromEnv ? (process.env.SENSU_AGENT_ID ?? 'unknown-agent') : 'unknown-agent');
|
|
514
|
+
this.orgId =
|
|
515
|
+
opts.orgId ??
|
|
516
|
+
(fromEnv ? (process.env.SENSU_ORG_ID ?? '') : '');
|
|
517
|
+
this.batchSize = opts.batchSize ?? 10;
|
|
518
|
+
this.flushIntervalMs = opts.flushIntervalMs ?? 2000;
|
|
519
|
+
this.disabled = opts.disabled ?? false;
|
|
520
|
+
this.disableLivePricing = opts.disableLivePricing ?? false;
|
|
521
|
+
this.onLoopDetected = opts.onLoopDetected;
|
|
522
|
+
this.loopThreshold = opts.loopThreshold ?? 5;
|
|
523
|
+
if (!this.disabled) {
|
|
524
|
+
this.flushTimer = setInterval(() => {
|
|
525
|
+
void this.flush();
|
|
526
|
+
}, this.flushIntervalMs);
|
|
527
|
+
if (this.flushTimer.unref)
|
|
528
|
+
this.flushTimer.unref();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/** Enqueue an event for batched sending */
|
|
532
|
+
enqueue(event) {
|
|
533
|
+
if (this.disabled)
|
|
534
|
+
return;
|
|
535
|
+
this.buffer.push(event);
|
|
536
|
+
if (this.buffer.length >= this.batchSize) {
|
|
537
|
+
void this.flush();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/** Flush all buffered events to the Sensu API */
|
|
541
|
+
async flush() {
|
|
542
|
+
if (this.disabled || this.buffer.length === 0)
|
|
543
|
+
return;
|
|
544
|
+
const events = this.buffer.splice(0);
|
|
545
|
+
try {
|
|
546
|
+
const res = await fetch(`${this.baseUrl}/api/v1/events`, {
|
|
547
|
+
method: 'POST',
|
|
548
|
+
headers: {
|
|
549
|
+
'Content-Type': 'application/json',
|
|
550
|
+
'X-API-Key': this.apiKey,
|
|
551
|
+
},
|
|
552
|
+
body: JSON.stringify({ events }),
|
|
553
|
+
});
|
|
554
|
+
if (!res.ok) {
|
|
555
|
+
const body = await res.text();
|
|
556
|
+
console.error(`[sensu:sdk] flush failed ${res.status}: ${body}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
// Re-queue on network error (best-effort)
|
|
561
|
+
console.error('[sensu:sdk] flush network error:', err);
|
|
562
|
+
this.buffer.unshift(...events);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/** Track a tool call for loop detection; fires onLoopDetected when threshold is reached. */
|
|
566
|
+
notifyToolCall(runId, toolName) {
|
|
567
|
+
if (!this.onLoopDetected)
|
|
568
|
+
return;
|
|
569
|
+
let runMap = this.runToolCallCounts.get(runId);
|
|
570
|
+
if (!runMap) {
|
|
571
|
+
runMap = new Map();
|
|
572
|
+
this.runToolCallCounts.set(runId, runMap);
|
|
573
|
+
}
|
|
574
|
+
const count = (runMap.get(toolName) ?? 0) + 1;
|
|
575
|
+
runMap.set(toolName, count);
|
|
576
|
+
if (count >= this.loopThreshold) {
|
|
577
|
+
this.onLoopDetected(toolName, count);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/** Remove per-run loop counters when the run ends to avoid memory leaks. */
|
|
581
|
+
clearRunLoopState(runId) {
|
|
582
|
+
this.runToolCallCounts.delete(runId);
|
|
583
|
+
}
|
|
584
|
+
/** Start a new agent run */
|
|
585
|
+
startRun(opts = {}) {
|
|
586
|
+
const runId = opts.runId ?? randomUUID();
|
|
587
|
+
const sessionId = opts.sessionId ?? randomUUID();
|
|
588
|
+
const traceId = randomUUID();
|
|
589
|
+
const spanId = randomUUID();
|
|
590
|
+
this.enqueue({
|
|
591
|
+
event_id: randomUUID(),
|
|
592
|
+
event_type: 'agent.run.started',
|
|
593
|
+
timestamp: new Date().toISOString(),
|
|
594
|
+
org_id: this.orgId,
|
|
595
|
+
agent_id: this.agentId,
|
|
596
|
+
session_id: sessionId,
|
|
597
|
+
run_id: runId,
|
|
598
|
+
trace_id: traceId,
|
|
599
|
+
span_id: spanId,
|
|
600
|
+
run_type: opts.runType,
|
|
601
|
+
end_user_id: opts.endUserId,
|
|
602
|
+
});
|
|
603
|
+
return new RunHandle(this, {
|
|
604
|
+
runId,
|
|
605
|
+
sessionId,
|
|
606
|
+
agentId: this.agentId,
|
|
607
|
+
orgId: this.orgId,
|
|
608
|
+
traceId,
|
|
609
|
+
spanId,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Spawn a child agent run from within a parent run.
|
|
614
|
+
* Emits `agent.spawned` on the parent and returns a RunHandle for the child.
|
|
615
|
+
*/
|
|
616
|
+
spawnRun(parentRun, opts) {
|
|
617
|
+
const childRunId = opts.childRunId ?? randomUUID();
|
|
618
|
+
const childAgentId = opts.childAgentId;
|
|
619
|
+
const sessionId = opts.sessionId ?? parentRun.sessionId;
|
|
620
|
+
const traceId = parentRun.traceId;
|
|
621
|
+
const spanId = randomUUID();
|
|
622
|
+
// Emit agent.spawned on the parent run
|
|
623
|
+
this.enqueue({
|
|
624
|
+
event_id: randomUUID(),
|
|
625
|
+
event_type: 'agent.spawned',
|
|
626
|
+
timestamp: new Date().toISOString(),
|
|
627
|
+
org_id: this.orgId,
|
|
628
|
+
agent_id: parentRun.agentId,
|
|
629
|
+
session_id: sessionId,
|
|
630
|
+
run_id: parentRun.runId,
|
|
631
|
+
trace_id: traceId,
|
|
632
|
+
span_id: spanId,
|
|
633
|
+
child_run_id: childRunId,
|
|
634
|
+
child_agent_id: childAgentId,
|
|
635
|
+
spawn_reason: opts.spawnReason,
|
|
636
|
+
});
|
|
637
|
+
// Emit agent.run.started for the child run (child agent emits this itself in practice,
|
|
638
|
+
// but the SDK can also do it on behalf of known child agents)
|
|
639
|
+
this.enqueue({
|
|
640
|
+
event_id: randomUUID(),
|
|
641
|
+
event_type: 'agent.run.started',
|
|
642
|
+
timestamp: new Date().toISOString(),
|
|
643
|
+
org_id: this.orgId,
|
|
644
|
+
agent_id: childAgentId,
|
|
645
|
+
session_id: sessionId,
|
|
646
|
+
run_id: childRunId,
|
|
647
|
+
trace_id: traceId,
|
|
648
|
+
span_id: randomUUID(),
|
|
649
|
+
run_type: opts.runType,
|
|
650
|
+
});
|
|
651
|
+
return new RunHandle(this, {
|
|
652
|
+
runId: childRunId,
|
|
653
|
+
sessionId,
|
|
654
|
+
agentId: childAgentId,
|
|
655
|
+
orgId: this.orgId,
|
|
656
|
+
traceId,
|
|
657
|
+
spanId,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
/** Explicitly start a session (sets channel and end_user_id) */
|
|
661
|
+
startSession(opts = {}) {
|
|
662
|
+
const sessionId = opts.sessionId ?? randomUUID();
|
|
663
|
+
const traceId = randomUUID();
|
|
664
|
+
const spanId = randomUUID();
|
|
665
|
+
this.enqueue({
|
|
666
|
+
event_id: randomUUID(),
|
|
667
|
+
event_type: 'session.started',
|
|
668
|
+
timestamp: new Date().toISOString(),
|
|
669
|
+
org_id: this.orgId,
|
|
670
|
+
agent_id: this.agentId,
|
|
671
|
+
session_id: sessionId,
|
|
672
|
+
run_id: sessionId, // run_id required by base schema; reuse session_id as placeholder
|
|
673
|
+
trace_id: traceId,
|
|
674
|
+
span_id: spanId,
|
|
675
|
+
channel: opts.channel,
|
|
676
|
+
end_user_id: opts.endUserId,
|
|
677
|
+
});
|
|
678
|
+
return sessionId;
|
|
679
|
+
}
|
|
680
|
+
/** Resume a previous session */
|
|
681
|
+
resumeSession(opts) {
|
|
682
|
+
const sessionId = opts.sessionId ?? randomUUID();
|
|
683
|
+
const traceId = randomUUID();
|
|
684
|
+
const spanId = randomUUID();
|
|
685
|
+
this.enqueue({
|
|
686
|
+
event_id: randomUUID(),
|
|
687
|
+
event_type: 'session.resumed',
|
|
688
|
+
timestamp: new Date().toISOString(),
|
|
689
|
+
org_id: this.orgId,
|
|
690
|
+
agent_id: this.agentId,
|
|
691
|
+
session_id: sessionId,
|
|
692
|
+
run_id: sessionId,
|
|
693
|
+
trace_id: traceId,
|
|
694
|
+
span_id: spanId,
|
|
695
|
+
resumed_from_session_id: opts.resumedFromSessionId,
|
|
696
|
+
channel: opts.channel,
|
|
697
|
+
end_user_id: opts.endUserId,
|
|
698
|
+
});
|
|
699
|
+
return sessionId;
|
|
700
|
+
}
|
|
701
|
+
/** Record a prompt version deployment (org-level event, not tied to a run) */
|
|
702
|
+
deployPromptVersion(opts) {
|
|
703
|
+
this.enqueue({
|
|
704
|
+
event_id: randomUUID(),
|
|
705
|
+
event_type: 'prompt.version.deployed',
|
|
706
|
+
timestamp: new Date().toISOString(),
|
|
707
|
+
org_id: this.orgId,
|
|
708
|
+
agent_id: this.agentId,
|
|
709
|
+
session_id: 'system',
|
|
710
|
+
run_id: 'system',
|
|
711
|
+
trace_id: randomUUID(),
|
|
712
|
+
span_id: randomUUID(),
|
|
713
|
+
template_id: opts.templateId,
|
|
714
|
+
new_version: opts.newVersion,
|
|
715
|
+
old_version: opts.oldVersion,
|
|
716
|
+
deployed_by: opts.deployedBy,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Resolve per-1M-token pricing for a model.
|
|
721
|
+
* Fetches from the Senzu API on first use and caches for the session lifetime.
|
|
722
|
+
* Falls back to the bundled MODEL_PRICING table if the API is unreachable or the
|
|
723
|
+
* model is unknown, and to a near-zero sentinel if it's missing from both.
|
|
724
|
+
*/
|
|
725
|
+
async resolvePricing(provider, model) {
|
|
726
|
+
if (this.disableLivePricing || this.disabled || !this.apiKey) {
|
|
727
|
+
return MODEL_PRICING[model] ?? [0.001, 0.002];
|
|
728
|
+
}
|
|
729
|
+
const key = `${provider}:${model}`;
|
|
730
|
+
if (!this.pricingCache.has(key)) {
|
|
731
|
+
try {
|
|
732
|
+
const res = await fetch(`${this.baseUrl}/api/v1/pricing/models/${encodeURIComponent(provider)}/${encodeURIComponent(model)}`, { headers: { 'X-API-Key': this.apiKey } });
|
|
733
|
+
if (res.ok) {
|
|
734
|
+
const data = (await res.json());
|
|
735
|
+
if (data.inputPricePer1mTokens != null && data.outputPricePer1mTokens != null) {
|
|
736
|
+
this.pricingCache.set(key, [data.inputPricePer1mTokens, data.outputPricePer1mTokens]);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
// Network error — fall through to local fallback
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return this.pricingCache.get(key) ?? MODEL_PRICING[model] ?? [0.001, 0.002];
|
|
745
|
+
}
|
|
746
|
+
destroy() {
|
|
747
|
+
if (this.flushTimer)
|
|
748
|
+
clearInterval(this.flushTimer);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
// Helpers
|
|
753
|
+
// ---------------------------------------------------------------------------
|
|
754
|
+
// Pricing per 1M tokens [input, output] in USD
|
|
755
|
+
const MODEL_PRICING = {
|
|
756
|
+
'claude-opus-4-6': [15.00, 75.00],
|
|
757
|
+
'claude-sonnet-4-6': [3.00, 15.00],
|
|
758
|
+
'claude-haiku-4-5-20251001': [0.80, 4.00],
|
|
759
|
+
'claude-3-5-sonnet-20241022': [3.00, 15.00],
|
|
760
|
+
'claude-3-5-haiku-20241022': [0.80, 4.00],
|
|
761
|
+
'claude-3-opus-20240229': [15.00, 75.00],
|
|
762
|
+
'gpt-4o': [2.50, 10.00],
|
|
763
|
+
'gpt-4o-mini': [0.15, 0.60],
|
|
764
|
+
'gpt-4-turbo': [10.00, 30.00],
|
|
765
|
+
};
|
|
766
|
+
function estimateCost(model, inputTokens, outputTokens) {
|
|
767
|
+
const pricing = MODEL_PRICING[model];
|
|
768
|
+
if (!pricing)
|
|
769
|
+
return 0;
|
|
770
|
+
const [inputPrice, outputPrice] = pricing;
|
|
771
|
+
return (inputTokens / 1_000_000) * inputPrice + (outputTokens / 1_000_000) * outputPrice;
|
|
772
|
+
}
|
|
773
|
+
function extractUsage(result, model) {
|
|
774
|
+
if (!result || typeof result !== 'object')
|
|
775
|
+
return {};
|
|
776
|
+
const r = result;
|
|
777
|
+
// Anthropic shape: { usage: { input_tokens, output_tokens, cache_read_input_tokens } }
|
|
778
|
+
if (r['usage'] && typeof r['usage'] === 'object') {
|
|
779
|
+
const u = r['usage'];
|
|
780
|
+
const inputTokens = num(u['input_tokens']) ?? 0;
|
|
781
|
+
const outputTokens = num(u['output_tokens']) ?? 0;
|
|
782
|
+
return {
|
|
783
|
+
input_tokens: inputTokens,
|
|
784
|
+
output_tokens: outputTokens,
|
|
785
|
+
cached_input_tokens: num(u['cache_read_input_tokens']),
|
|
786
|
+
total_tokens: inputTokens + outputTokens,
|
|
787
|
+
cost_usd_estimate: estimateCost(model, inputTokens, outputTokens),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
// OpenAI shape: { choices: [...], usage: { prompt_tokens, completion_tokens, total_tokens } }
|
|
791
|
+
if (r['choices'] && r['usage'] && typeof r['usage'] === 'object') {
|
|
792
|
+
const u = r['usage'];
|
|
793
|
+
const inputTokens = num(u['prompt_tokens']) ?? 0;
|
|
794
|
+
const outputTokens = num(u['completion_tokens']) ?? 0;
|
|
795
|
+
return {
|
|
796
|
+
input_tokens: inputTokens,
|
|
797
|
+
output_tokens: outputTokens,
|
|
798
|
+
total_tokens: num(u['total_tokens']),
|
|
799
|
+
cost_usd_estimate: estimateCost(model, inputTokens, outputTokens),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
return {};
|
|
803
|
+
}
|
|
804
|
+
function num(v) {
|
|
805
|
+
return typeof v === 'number' ? v : undefined;
|
|
806
|
+
}
|
|
807
|
+
function estimateBytes(v) {
|
|
808
|
+
try {
|
|
809
|
+
return JSON.stringify(v)?.length ?? 0;
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
return 0;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// Extract text from common streaming chunk shapes (Anthropic / OpenAI)
|
|
816
|
+
function extractStreamChunkText(chunk) {
|
|
817
|
+
if (typeof chunk === 'string')
|
|
818
|
+
return chunk;
|
|
819
|
+
if (typeof chunk !== 'object' || chunk === null)
|
|
820
|
+
return '';
|
|
821
|
+
const c = chunk;
|
|
822
|
+
// Anthropic: { type: 'content_block_delta', delta: { type: 'text_delta', text: '...' } }
|
|
823
|
+
if (c['type'] === 'content_block_delta') {
|
|
824
|
+
const delta = c['delta'];
|
|
825
|
+
if (typeof delta?.['text'] === 'string')
|
|
826
|
+
return delta['text'];
|
|
827
|
+
}
|
|
828
|
+
// OpenAI: { choices: [{ delta: { content: '...' } }] }
|
|
829
|
+
const choices = c['choices'];
|
|
830
|
+
if (Array.isArray(choices) && choices.length > 0) {
|
|
831
|
+
const delta = choices[0]['delta'];
|
|
832
|
+
if (typeof delta?.['content'] === 'string')
|
|
833
|
+
return delta['content'];
|
|
834
|
+
}
|
|
835
|
+
return '';
|
|
836
|
+
}
|
|
837
|
+
//# sourceMappingURL=client.js.map
|