@eval-studio/flue 0.0.0-stage → 1.1.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/CHANGELOG.md +40 -0
- package/README.md +254 -2
- package/dist/chunk-LKO4T54N.js +131 -0
- package/dist/index.d.ts +195 -0
- package/dist/index.js +708 -0
- package/dist/legacy.d.ts +180 -0
- package/dist/legacy.js +449 -0
- package/dist/version-DU6_w67Z.d.ts +40 -0
- package/package.json +30 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentVersion,
|
|
3
|
+
agentVersionHash,
|
|
4
|
+
toolManifestEntry
|
|
5
|
+
} from "./chunk-LKO4T54N.js";
|
|
6
|
+
|
|
7
|
+
// src/feedback.ts
|
|
8
|
+
import { createHash, randomUUID } from "crypto";
|
|
9
|
+
|
|
10
|
+
// ../schema/src/feedback-wire.ts
|
|
11
|
+
var FEEDBACK_EVENT_TYPES = [
|
|
12
|
+
"approval.approved",
|
|
13
|
+
"approval.rejected",
|
|
14
|
+
"approval.revised",
|
|
15
|
+
"case.takeover",
|
|
16
|
+
"case.handback",
|
|
17
|
+
"case.hold",
|
|
18
|
+
"edit.sent"
|
|
19
|
+
];
|
|
20
|
+
var FEEDBACK_REASON_CODES = [
|
|
21
|
+
"wrong_facts",
|
|
22
|
+
"wrong_action",
|
|
23
|
+
"tone",
|
|
24
|
+
"policy",
|
|
25
|
+
"duplicate",
|
|
26
|
+
"other"
|
|
27
|
+
];
|
|
28
|
+
var isPlainObject = (v) => v != null && typeof v === "object" && !Array.isArray(v);
|
|
29
|
+
var MAX_FEEDBACK_BODY_BYTES = 1024 * 1024;
|
|
30
|
+
var MAX_ATTRIBUTE_KEYS = 32;
|
|
31
|
+
var MAX_FEEDBACK_ID = 256;
|
|
32
|
+
var MAX_ATTRIBUTE_VALUE = 1024;
|
|
33
|
+
var MAX_DECISION_LATENCY_MS = Number.MAX_SAFE_INTEGER;
|
|
34
|
+
var DECISION_LATENCY_S_ATTRIBUTE = "decision_latency_s";
|
|
35
|
+
var MAX_FUTURE_MS = 864e5;
|
|
36
|
+
var EDIT_TYPES = /* @__PURE__ */ new Set(["edit.sent", "approval.revised"]);
|
|
37
|
+
var FORBIDDEN_KEYS = [
|
|
38
|
+
"note",
|
|
39
|
+
"note_text",
|
|
40
|
+
"comment",
|
|
41
|
+
"actor_id",
|
|
42
|
+
"actor",
|
|
43
|
+
"user_id",
|
|
44
|
+
"email"
|
|
45
|
+
];
|
|
46
|
+
var isNonNegInt = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 2 ** 31 - 1;
|
|
47
|
+
function decisionLatencyMsOf(ms, attributes) {
|
|
48
|
+
if (ms != null && ms !== "") {
|
|
49
|
+
const n = typeof ms === "string" ? Number(ms) : ms;
|
|
50
|
+
if (typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= MAX_DECISION_LATENCY_MS)
|
|
51
|
+
return Math.round(n);
|
|
52
|
+
}
|
|
53
|
+
const raw = attributes?.[DECISION_LATENCY_S_ATTRIBUTE];
|
|
54
|
+
const s = typeof raw === "string" && /^\s*\d+(\.\d+)?\s*$/.test(raw) ? Number(raw) : raw;
|
|
55
|
+
if (typeof s === "number" && Number.isFinite(s) && s >= 0 && s * 1e3 <= MAX_DECISION_LATENCY_MS)
|
|
56
|
+
return Math.round(s * 1e3);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function validateFeedbackEvent(raw, now = Date.now()) {
|
|
60
|
+
if (!isPlainObject(raw)) return { ok: false, errors: ["not an object"] };
|
|
61
|
+
const errors = [];
|
|
62
|
+
const str = (key, opts = {}) => {
|
|
63
|
+
const v = raw[key];
|
|
64
|
+
if (v == null) {
|
|
65
|
+
if (opts.required) errors.push(`${key} is required`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
69
|
+
errors.push(`${key} must be a non-empty string`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (v.length > (opts.max ?? MAX_FEEDBACK_ID)) {
|
|
73
|
+
errors.push(`${key} exceeds ${opts.max ?? MAX_FEEDBACK_ID} characters`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (opts.pattern && !opts.pattern.test(v)) {
|
|
77
|
+
errors.push(`${key} has an invalid format`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return v;
|
|
81
|
+
};
|
|
82
|
+
for (const k of FORBIDDEN_KEYS)
|
|
83
|
+
if (k in raw)
|
|
84
|
+
errors.push(
|
|
85
|
+
`${k} is not accepted: send note_present and a hashed actor_hash, never note text or raw user ids`
|
|
86
|
+
);
|
|
87
|
+
const id = str("id", { required: true, max: 128, pattern: /^[\w.:-]+$/ });
|
|
88
|
+
const type = raw.type;
|
|
89
|
+
if (!FEEDBACK_EVENT_TYPES.includes(type))
|
|
90
|
+
errors.push(`type must be one of ${FEEDBACK_EVENT_TYPES.join(", ")}`);
|
|
91
|
+
let occurredAt = null;
|
|
92
|
+
const occurred = str("occurred_at", { required: true, max: 64 });
|
|
93
|
+
if (occurred) {
|
|
94
|
+
const ms = Date.parse(occurred);
|
|
95
|
+
if (!Number.isFinite(ms))
|
|
96
|
+
errors.push("occurred_at must be an ISO timestamp");
|
|
97
|
+
else if (ms > now + MAX_FUTURE_MS)
|
|
98
|
+
errors.push("occurred_at is more than a day in the future");
|
|
99
|
+
else occurredAt = new Date(ms).toISOString();
|
|
100
|
+
}
|
|
101
|
+
const conversationId = str("conversation_id", { required: true });
|
|
102
|
+
const submissionId = str("submission_id");
|
|
103
|
+
const toolCallId = str("tool_call_id");
|
|
104
|
+
const outputId = str("output_id");
|
|
105
|
+
const agentName = str("agent_name");
|
|
106
|
+
const actionType = str("action_type");
|
|
107
|
+
const traceId = str("trace_id", { pattern: /^[0-9a-fA-F]{32}$/ })?.toLowerCase() ?? null;
|
|
108
|
+
const spanId = str("span_id", { pattern: /^[0-9a-fA-F]{16}$/ })?.toLowerCase() ?? null;
|
|
109
|
+
const actorHash = str("actor_hash", { pattern: /^[0-9a-fA-F]{64}$/ })?.toLowerCase() ?? null;
|
|
110
|
+
if (raw.actor_hash != null && actorHash == null)
|
|
111
|
+
errors.push("actor_hash must be a sha256 hex digest (hash it client-side)");
|
|
112
|
+
let reasonCode = null;
|
|
113
|
+
if (raw.reason_code != null) {
|
|
114
|
+
if (FEEDBACK_REASON_CODES.includes(raw.reason_code))
|
|
115
|
+
reasonCode = raw.reason_code;
|
|
116
|
+
else
|
|
117
|
+
errors.push(
|
|
118
|
+
`reason_code must be one of ${FEEDBACK_REASON_CODES.join(", ")}`
|
|
119
|
+
);
|
|
120
|
+
} else if (type === "approval.rejected") {
|
|
121
|
+
errors.push("reason_code is required for approval.rejected");
|
|
122
|
+
}
|
|
123
|
+
let notePresent = false;
|
|
124
|
+
if (raw.note_present != null) {
|
|
125
|
+
if (typeof raw.note_present !== "boolean")
|
|
126
|
+
errors.push("note_present must be a boolean");
|
|
127
|
+
else notePresent = raw.note_present;
|
|
128
|
+
}
|
|
129
|
+
let edit = {
|
|
130
|
+
editCharsBefore: null,
|
|
131
|
+
editCharsAfter: null,
|
|
132
|
+
editDistance: null,
|
|
133
|
+
editRatio: null
|
|
134
|
+
};
|
|
135
|
+
if (raw.edit != null) {
|
|
136
|
+
const e = raw.edit;
|
|
137
|
+
if (!EDIT_TYPES.has(type))
|
|
138
|
+
errors.push("edit is only accepted on edit.sent and approval.revised");
|
|
139
|
+
if (!isPlainObject(e)) errors.push("edit must be an object");
|
|
140
|
+
else {
|
|
141
|
+
const before = e.chars_before;
|
|
142
|
+
const after = e.chars_after;
|
|
143
|
+
const distance = e.edit_distance;
|
|
144
|
+
const ratio = e.edit_ratio;
|
|
145
|
+
if (before != null && !isNonNegInt(before))
|
|
146
|
+
errors.push("edit.chars_before must be a non-negative integer");
|
|
147
|
+
if (after != null && !isNonNegInt(after))
|
|
148
|
+
errors.push("edit.chars_after must be a non-negative integer");
|
|
149
|
+
if (distance != null && !isNonNegInt(distance))
|
|
150
|
+
errors.push("edit.edit_distance must be a non-negative integer");
|
|
151
|
+
if (ratio != null && (typeof ratio !== "number" || !Number.isFinite(ratio) || ratio < 0 || ratio > 1))
|
|
152
|
+
errors.push("edit.edit_ratio must be a number between 0 and 1");
|
|
153
|
+
edit = {
|
|
154
|
+
editCharsBefore: isNonNegInt(before) ? before : null,
|
|
155
|
+
editCharsAfter: isNonNegInt(after) ? after : null,
|
|
156
|
+
editDistance: isNonNegInt(distance) ? distance : null,
|
|
157
|
+
editRatio: typeof ratio === "number" && ratio >= 0 && ratio <= 1 ? ratio : null
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
let decisionLatencyMs = null;
|
|
162
|
+
if (raw.decision_latency_ms != null) {
|
|
163
|
+
const ms = raw.decision_latency_ms;
|
|
164
|
+
if (typeof ms !== "number" || !Number.isInteger(ms) || ms < 0 || ms > MAX_DECISION_LATENCY_MS)
|
|
165
|
+
errors.push("decision_latency_ms must be a non-negative integer");
|
|
166
|
+
else decisionLatencyMs = ms;
|
|
167
|
+
}
|
|
168
|
+
const attributes = {};
|
|
169
|
+
if (raw.attributes != null) {
|
|
170
|
+
if (!isPlainObject(raw.attributes))
|
|
171
|
+
errors.push("attributes must be a flat object");
|
|
172
|
+
else {
|
|
173
|
+
const entries = Object.entries(raw.attributes);
|
|
174
|
+
if (entries.length > MAX_ATTRIBUTE_KEYS)
|
|
175
|
+
errors.push(`attributes has more than ${MAX_ATTRIBUTE_KEYS} keys`);
|
|
176
|
+
for (const [k, v] of entries.slice(0, MAX_ATTRIBUTE_KEYS)) {
|
|
177
|
+
if (k.length === 0 || k.length > 128) {
|
|
178
|
+
errors.push(`attributes key "${k.slice(0, 32)}" is empty or too long`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (typeof v === "string") {
|
|
182
|
+
if (v.length > MAX_ATTRIBUTE_VALUE)
|
|
183
|
+
errors.push(
|
|
184
|
+
`attributes.${k} exceeds ${MAX_ATTRIBUTE_VALUE} characters`
|
|
185
|
+
);
|
|
186
|
+
else attributes[k] = v;
|
|
187
|
+
} else if (typeof v === "boolean") attributes[k] = v;
|
|
188
|
+
else if (typeof v === "number" && Number.isFinite(v)) attributes[k] = v;
|
|
189
|
+
else errors.push(`attributes.${k} must be a string, number or boolean`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (decisionLatencyMs == null)
|
|
194
|
+
decisionLatencyMs = decisionLatencyMsOf(null, attributes);
|
|
195
|
+
if (errors.length > 0 || !id || !occurredAt || !conversationId)
|
|
196
|
+
return { ok: false, errors: errors.length ? errors : ["invalid event"] };
|
|
197
|
+
return {
|
|
198
|
+
ok: true,
|
|
199
|
+
event: {
|
|
200
|
+
id,
|
|
201
|
+
type,
|
|
202
|
+
occurredAt,
|
|
203
|
+
conversationId,
|
|
204
|
+
submissionId,
|
|
205
|
+
toolCallId,
|
|
206
|
+
outputId,
|
|
207
|
+
traceId,
|
|
208
|
+
spanId,
|
|
209
|
+
agentName,
|
|
210
|
+
actionType,
|
|
211
|
+
actorHash,
|
|
212
|
+
reasonCode,
|
|
213
|
+
notePresent,
|
|
214
|
+
...edit,
|
|
215
|
+
decisionLatencyMs,
|
|
216
|
+
attributes
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/feedback.ts
|
|
222
|
+
var FEEDBACK_TYPES = [
|
|
223
|
+
"approval.approved",
|
|
224
|
+
"approval.rejected",
|
|
225
|
+
"approval.revised",
|
|
226
|
+
"case.takeover",
|
|
227
|
+
"case.handback",
|
|
228
|
+
"case.hold",
|
|
229
|
+
"edit.sent"
|
|
230
|
+
];
|
|
231
|
+
var REASON_CODES = [
|
|
232
|
+
"wrong_facts",
|
|
233
|
+
"wrong_action",
|
|
234
|
+
"tone",
|
|
235
|
+
"policy",
|
|
236
|
+
"duplicate",
|
|
237
|
+
"other"
|
|
238
|
+
];
|
|
239
|
+
function hashActor(id, salt) {
|
|
240
|
+
if (!salt) throw new TypeError("hashActor needs a non-empty salt");
|
|
241
|
+
return createHash("sha256").update(`${salt}:${id}`).digest("hex");
|
|
242
|
+
}
|
|
243
|
+
function levenshtein(a, b) {
|
|
244
|
+
if (a === b) return 0;
|
|
245
|
+
if (a.length < b.length) [a, b] = [b, a];
|
|
246
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
247
|
+
for (let i = 1; i <= a.length; i++) {
|
|
248
|
+
const cur = [i];
|
|
249
|
+
for (let j = 1; j <= b.length; j++) {
|
|
250
|
+
cur[j] = Math.min(
|
|
251
|
+
prev[j] + 1,
|
|
252
|
+
cur[j - 1] + 1,
|
|
253
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
prev = cur;
|
|
257
|
+
}
|
|
258
|
+
return prev[b.length];
|
|
259
|
+
}
|
|
260
|
+
var MAX_DIFF_CHARS = 2e4;
|
|
261
|
+
function editStats(before, after) {
|
|
262
|
+
const charsBefore = before.length;
|
|
263
|
+
const charsAfter = after.length;
|
|
264
|
+
const longest = Math.max(charsBefore, charsAfter);
|
|
265
|
+
const editDistance = charsBefore > MAX_DIFF_CHARS || charsAfter > MAX_DIFF_CHARS ? before === after ? 0 : Math.max(Math.abs(charsBefore - charsAfter), 1) : levenshtein(before, after);
|
|
266
|
+
return {
|
|
267
|
+
charsBefore,
|
|
268
|
+
charsAfter,
|
|
269
|
+
editDistance,
|
|
270
|
+
editRatio: longest === 0 ? 0 : Math.min(1, editDistance / longest)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
var HEX64 = /^[0-9a-f]{64}$/i;
|
|
274
|
+
function toWireEvent(e) {
|
|
275
|
+
if (!FEEDBACK_TYPES.includes(e.type))
|
|
276
|
+
throw new TypeError(`unknown feedback type "${String(e.type)}"`);
|
|
277
|
+
if (!e.conversationId) throw new TypeError("conversationId is required");
|
|
278
|
+
if (e.type === "approval.rejected" && !e.reasonCode)
|
|
279
|
+
throw new TypeError("approval.rejected needs a reasonCode");
|
|
280
|
+
if (e.reasonCode && !REASON_CODES.includes(e.reasonCode))
|
|
281
|
+
throw new TypeError(`unknown reasonCode "${e.reasonCode}"`);
|
|
282
|
+
if (e.actorHash && !HEX64.test(e.actorHash))
|
|
283
|
+
throw new TypeError(
|
|
284
|
+
"actorHash must be a sha256 hex digest \u2014 use hashActor(id, salt), never a raw id"
|
|
285
|
+
);
|
|
286
|
+
if (e.edit && e.type !== "edit.sent" && e.type !== "approval.revised")
|
|
287
|
+
throw new TypeError("edit is only valid on edit.sent and approval.revised");
|
|
288
|
+
if (e.attributes && Object.keys(e.attributes).length > 32)
|
|
289
|
+
throw new TypeError("attributes may have at most 32 keys");
|
|
290
|
+
const occurred = e.occurredAt instanceof Date ? e.occurredAt.toISOString() : e.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
291
|
+
const wire = {
|
|
292
|
+
id: e.id ?? randomUUID(),
|
|
293
|
+
type: e.type,
|
|
294
|
+
occurred_at: occurred,
|
|
295
|
+
conversation_id: e.conversationId,
|
|
296
|
+
...e.submissionId ? { submission_id: e.submissionId } : {},
|
|
297
|
+
...e.toolCallId ? { tool_call_id: e.toolCallId } : {},
|
|
298
|
+
...e.outputId ? { output_id: e.outputId } : {},
|
|
299
|
+
...e.traceId ? { trace_id: e.traceId } : {},
|
|
300
|
+
...e.spanId ? { span_id: e.spanId } : {},
|
|
301
|
+
...e.agentName ? { agent_name: e.agentName } : {},
|
|
302
|
+
...e.actionType ? { action_type: e.actionType } : {},
|
|
303
|
+
...e.actorHash ? { actor_hash: e.actorHash.toLowerCase() } : {},
|
|
304
|
+
...e.reasonCode ? { reason_code: e.reasonCode } : {},
|
|
305
|
+
...e.notePresent != null ? { note_present: e.notePresent } : {},
|
|
306
|
+
...e.edit ? {
|
|
307
|
+
edit: {
|
|
308
|
+
...e.edit.charsBefore != null ? { chars_before: e.edit.charsBefore } : {},
|
|
309
|
+
...e.edit.charsAfter != null ? { chars_after: e.edit.charsAfter } : {},
|
|
310
|
+
...e.edit.editDistance != null ? { edit_distance: e.edit.editDistance } : {},
|
|
311
|
+
...e.edit.editRatio != null ? { edit_ratio: e.edit.editRatio } : {}
|
|
312
|
+
}
|
|
313
|
+
} : {},
|
|
314
|
+
...e.decisionLatencyMs != null ? { decision_latency_ms: Math.round(e.decisionLatencyMs) } : {},
|
|
315
|
+
...e.attributes ? { attributes: e.attributes } : {}
|
|
316
|
+
};
|
|
317
|
+
const checked = validateFeedbackEvent(wire);
|
|
318
|
+
if (!checked.ok)
|
|
319
|
+
throw new TypeError(`invalid feedback event: ${checked.errors.join("; ")}`);
|
|
320
|
+
return wire;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/instrumentation.ts
|
|
324
|
+
import { trace } from "@opentelemetry/api";
|
|
325
|
+
var EVAL_STUDIO_INSTRUMENTATION_KEY = /* @__PURE__ */ Symbol.for("@eval-studio/flue");
|
|
326
|
+
var ATTR_SUBJECT_VERSION_ID = "eval.subject_version_id";
|
|
327
|
+
var ATTR_VERSION_HASH = "eval.version_hash";
|
|
328
|
+
var ATTR_COST_USD = "flue.cost.usd";
|
|
329
|
+
var bits = (span) => span;
|
|
330
|
+
function isAgentSpan(span) {
|
|
331
|
+
if (!span.isRecording()) return false;
|
|
332
|
+
const name = bits(span).name;
|
|
333
|
+
return typeof name === "string" ? name.startsWith("invoke_agent") : true;
|
|
334
|
+
}
|
|
335
|
+
function spanAgentName(span) {
|
|
336
|
+
const v = bits(span).attributes?.["gen_ai.agent.name"];
|
|
337
|
+
return typeof v === "string" ? v : void 0;
|
|
338
|
+
}
|
|
339
|
+
var MAX_OPEN_TURNS = 1e4;
|
|
340
|
+
function createInstrumentation(lookup, options = {}) {
|
|
341
|
+
const otel = options.otel;
|
|
342
|
+
const costBridge = options.costBridge ?? true;
|
|
343
|
+
const chatSpans = /* @__PURE__ */ new Map();
|
|
344
|
+
const operationAgent = /* @__PURE__ */ new Map();
|
|
345
|
+
const opKey = (instanceId, operationId) => `${instanceId ?? ""}\0${operationId}`;
|
|
346
|
+
const stamp = (span, agentName) => {
|
|
347
|
+
if (!span || !agentName || !isAgentSpan(span)) return;
|
|
348
|
+
const version = lookup(agentName);
|
|
349
|
+
if (!version) return;
|
|
350
|
+
span.setAttribute(ATTR_VERSION_HASH, version.versionHash);
|
|
351
|
+
if (version.subjectVersionId)
|
|
352
|
+
span.setAttribute(ATTR_SUBJECT_VERSION_ID, version.subjectVersionId);
|
|
353
|
+
};
|
|
354
|
+
const own = (operation, ctx, next) => {
|
|
355
|
+
try {
|
|
356
|
+
if (operation.type === "agent") {
|
|
357
|
+
const span = trace.getActiveSpan();
|
|
358
|
+
stamp(
|
|
359
|
+
span,
|
|
360
|
+
ctx.agentName ?? operationAgent.get(opKey(ctx.instanceId, operation.operationId)) ?? (span ? spanAgentName(span) : void 0)
|
|
361
|
+
);
|
|
362
|
+
} else if (operation.type === "task") {
|
|
363
|
+
const span = trace.getActiveSpan();
|
|
364
|
+
stamp(span, span ? spanAgentName(span) : void 0);
|
|
365
|
+
} else if (operation.type === "model" && costBridge) {
|
|
366
|
+
const span = trace.getActiveSpan();
|
|
367
|
+
if (span?.isRecording()) {
|
|
368
|
+
if (chatSpans.size >= MAX_OPEN_TURNS) chatSpans.clear();
|
|
369
|
+
chatSpans.set(operation.turnId, span);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
return next();
|
|
375
|
+
};
|
|
376
|
+
const observe = (event, ctx) => {
|
|
377
|
+
try {
|
|
378
|
+
const e = event;
|
|
379
|
+
if (e.type === "operation_start" && e.operationId && e.agentName) {
|
|
380
|
+
if (operationAgent.size >= MAX_OPEN_TURNS) operationAgent.clear();
|
|
381
|
+
operationAgent.set(opKey(e.instanceId, e.operationId), e.agentName);
|
|
382
|
+
} else if (e.type === "operation" && e.operationId) {
|
|
383
|
+
operationAgent.delete(opKey(e.instanceId, e.operationId));
|
|
384
|
+
} else if (e.type === "turn" && e.turnId) {
|
|
385
|
+
const span = chatSpans.get(e.turnId);
|
|
386
|
+
chatSpans.delete(e.turnId);
|
|
387
|
+
const cost = e.response?.usage?.cost?.total;
|
|
388
|
+
if (span && typeof cost === "number" && Number.isFinite(cost) && bits(span).ended !== true && bits(span).attributes?.[ATTR_COST_USD] == null)
|
|
389
|
+
span.setAttribute(ATTR_COST_USD, cost);
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
return otel?.observe(event, ctx);
|
|
394
|
+
};
|
|
395
|
+
return {
|
|
396
|
+
key: EVAL_STUDIO_INSTRUMENTATION_KEY,
|
|
397
|
+
observe,
|
|
398
|
+
interceptor: otel ? (operation, ctx, next) => otel.interceptor(operation, ctx, () => own(operation, ctx, next)) : own,
|
|
399
|
+
dispose: async () => {
|
|
400
|
+
chatSpans.clear();
|
|
401
|
+
operationAgent.clear();
|
|
402
|
+
await otel?.dispose();
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/transport.ts
|
|
408
|
+
var defaultSleep = (ms) => new Promise((resolve) => {
|
|
409
|
+
const t = setTimeout(resolve, ms);
|
|
410
|
+
t.unref?.();
|
|
411
|
+
});
|
|
412
|
+
var retryableStatus = (s) => s === 408 || s === 429 || s >= 500;
|
|
413
|
+
function backoffDelay(attempt, opts, retryAfterSeconds) {
|
|
414
|
+
const cap = opts.maxDelayMs ?? 3e4;
|
|
415
|
+
if (retryAfterSeconds != null && Number.isFinite(retryAfterSeconds))
|
|
416
|
+
return Math.min(cap, Math.max(0, retryAfterSeconds * 1e3));
|
|
417
|
+
const exp = (opts.baseDelayMs ?? 500) * 2 ** attempt;
|
|
418
|
+
return Math.min(cap, exp / 2 + (opts.random ?? Math.random)() * (exp / 2));
|
|
419
|
+
}
|
|
420
|
+
async function attemptPost(url, projectKey, body, opts) {
|
|
421
|
+
const doFetch = opts.fetch ?? fetch;
|
|
422
|
+
const controller = new AbortController();
|
|
423
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 1e4);
|
|
424
|
+
timer.unref?.();
|
|
425
|
+
try {
|
|
426
|
+
const res = await doFetch(url, {
|
|
427
|
+
method: "POST",
|
|
428
|
+
headers: {
|
|
429
|
+
authorization: `Bearer ${projectKey}`,
|
|
430
|
+
"content-type": "application/json"
|
|
431
|
+
},
|
|
432
|
+
body: JSON.stringify(body),
|
|
433
|
+
signal: controller.signal
|
|
434
|
+
});
|
|
435
|
+
const text = await res.text();
|
|
436
|
+
let parsed = text;
|
|
437
|
+
try {
|
|
438
|
+
parsed = text ? JSON.parse(text) : null;
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
if (res.ok) return { ok: true, status: res.status, body: parsed };
|
|
442
|
+
const retryAfter = Number(res.headers.get("retry-after") ?? NaN);
|
|
443
|
+
const message = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `HTTP ${res.status}`;
|
|
444
|
+
return {
|
|
445
|
+
ok: false,
|
|
446
|
+
status: res.status,
|
|
447
|
+
error: message,
|
|
448
|
+
retryable: retryableStatus(res.status),
|
|
449
|
+
body: parsed,
|
|
450
|
+
...Number.isFinite(retryAfter) ? { retryAfter } : {}
|
|
451
|
+
};
|
|
452
|
+
} catch (e) {
|
|
453
|
+
return {
|
|
454
|
+
ok: false,
|
|
455
|
+
status: null,
|
|
456
|
+
error: e instanceof Error ? e.message : String(e),
|
|
457
|
+
retryable: true
|
|
458
|
+
};
|
|
459
|
+
} finally {
|
|
460
|
+
clearTimeout(timer);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async function postWithRetry(url, projectKey, body, opts = {}) {
|
|
464
|
+
const attempts = Math.max(1, opts.maxAttempts ?? 5);
|
|
465
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
466
|
+
let last = {
|
|
467
|
+
ok: false,
|
|
468
|
+
status: null,
|
|
469
|
+
error: "not sent",
|
|
470
|
+
retryable: true
|
|
471
|
+
};
|
|
472
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
473
|
+
const r = await attemptPost(url, projectKey, body, opts);
|
|
474
|
+
if (r.ok || !r.retryable) {
|
|
475
|
+
const { retryAfter: _ra, ...result } = r;
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
last = { ok: false, status: r.status, error: r.error, retryable: true };
|
|
479
|
+
if (attempt < attempts - 1)
|
|
480
|
+
await sleep(backoffDelay(attempt, opts, r.retryAfter));
|
|
481
|
+
}
|
|
482
|
+
return last;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/client.ts
|
|
486
|
+
var HEX_SHA = /^[0-9a-f]{7,40}$/i;
|
|
487
|
+
var MAX_BATCH_BYTES = 9e5;
|
|
488
|
+
var BATCH_ENVELOPE_BYTES = '{"events":[]}'.length;
|
|
489
|
+
function invalidIndexes(res, size) {
|
|
490
|
+
if (res.status !== 400) return null;
|
|
491
|
+
const invalid = res.body?.invalid;
|
|
492
|
+
if (!Array.isArray(invalid)) return null;
|
|
493
|
+
const out = /* @__PURE__ */ new Set();
|
|
494
|
+
for (const item of invalid) {
|
|
495
|
+
const index = item?.index;
|
|
496
|
+
if (typeof index === "number" && Number.isInteger(index) && index >= 0 && index < size)
|
|
497
|
+
out.add(index);
|
|
498
|
+
}
|
|
499
|
+
return out.size > 0 ? out : null;
|
|
500
|
+
}
|
|
501
|
+
function createEvalStudio(options = {}) {
|
|
502
|
+
const endpoint = (options.endpoint ?? process.env.EVAL_STUDIO_URL)?.replace(
|
|
503
|
+
/\/+$/,
|
|
504
|
+
""
|
|
505
|
+
);
|
|
506
|
+
const projectKey = options.projectKey ?? process.env.EVAL_STUDIO_PROJECT_KEY ?? process.env.EVAL_STUDIO_API_KEY;
|
|
507
|
+
const enabled = Boolean(endpoint && projectKey);
|
|
508
|
+
const serviceVersion = options.serviceVersion ?? process.env.OTEL_SERVICE_VERSION ?? process.env.GIT_SHA;
|
|
509
|
+
const batchSize = Math.min(500, Math.max(1, options.batchSize ?? 100));
|
|
510
|
+
const maxQueue = Math.max(batchSize, options.maxQueueSize ?? 1e4);
|
|
511
|
+
const report = (e) => {
|
|
512
|
+
try {
|
|
513
|
+
if (options.onError) options.onError(e);
|
|
514
|
+
else console.warn(`[eval-studio] ${e.kind}: ${e.message}`);
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const registry = /* @__PURE__ */ new Map();
|
|
519
|
+
let queue = [];
|
|
520
|
+
let closed = false;
|
|
521
|
+
let inFlight = null;
|
|
522
|
+
let followUp = null;
|
|
523
|
+
let timer = null;
|
|
524
|
+
const ensureTimer = () => {
|
|
525
|
+
if (timer || !enabled) return;
|
|
526
|
+
timer = setInterval(() => {
|
|
527
|
+
if (queue.length > 0 && !inFlight) void flush();
|
|
528
|
+
}, options.flushIntervalMs ?? 2e3);
|
|
529
|
+
timer.unref?.();
|
|
530
|
+
};
|
|
531
|
+
async function registerAgentVersion(def) {
|
|
532
|
+
const version = agentVersion(def);
|
|
533
|
+
const known = registry.get(def.agentName);
|
|
534
|
+
const entry = {
|
|
535
|
+
...version,
|
|
536
|
+
subjectVersionId: known?.versionHash === version.versionHash ? known.subjectVersionId : null
|
|
537
|
+
};
|
|
538
|
+
registry.set(def.agentName, entry);
|
|
539
|
+
if (!enabled) return entry;
|
|
540
|
+
const metadata = {
|
|
541
|
+
tool_names: version.toolNames,
|
|
542
|
+
...options.sendInstructions === false ? {} : { instructions: def.instructions },
|
|
543
|
+
...serviceVersion ? { service_version: serviceVersion } : {}
|
|
544
|
+
};
|
|
545
|
+
const res = await postWithRetry(
|
|
546
|
+
`${endpoint}/v1/subject-versions`,
|
|
547
|
+
projectKey,
|
|
548
|
+
{
|
|
549
|
+
agent_name: version.agentName,
|
|
550
|
+
version_hash: version.versionHash,
|
|
551
|
+
model: version.model,
|
|
552
|
+
instructions_hash: version.instructionsHash,
|
|
553
|
+
tools_hash: version.toolsHash,
|
|
554
|
+
...serviceVersion && HEX_SHA.test(serviceVersion) ? { git_sha: serviceVersion } : {},
|
|
555
|
+
metadata
|
|
556
|
+
},
|
|
557
|
+
options
|
|
558
|
+
);
|
|
559
|
+
if (!res.ok) {
|
|
560
|
+
report({
|
|
561
|
+
kind: "register",
|
|
562
|
+
message: `${def.agentName}: ${res.error}`,
|
|
563
|
+
status: res.status
|
|
564
|
+
});
|
|
565
|
+
return entry;
|
|
566
|
+
}
|
|
567
|
+
const id = res.body.subject_version_id;
|
|
568
|
+
const current = registry.get(def.agentName);
|
|
569
|
+
if (typeof id === "string" && current?.versionHash === version.versionHash)
|
|
570
|
+
current.subjectVersionId = id;
|
|
571
|
+
return { ...entry, subjectVersionId: typeof id === "string" ? id : null };
|
|
572
|
+
}
|
|
573
|
+
function enqueue(events) {
|
|
574
|
+
if (closed) {
|
|
575
|
+
report({
|
|
576
|
+
kind: "dropped",
|
|
577
|
+
message: "feedback after shutdown()",
|
|
578
|
+
count: events.length
|
|
579
|
+
});
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const wire = events.map(toWireEvent);
|
|
583
|
+
if (!enabled) return;
|
|
584
|
+
queue.push(...wire);
|
|
585
|
+
if (queue.length > maxQueue) {
|
|
586
|
+
const over = queue.length - maxQueue;
|
|
587
|
+
queue = queue.slice(over);
|
|
588
|
+
report({
|
|
589
|
+
kind: "dropped",
|
|
590
|
+
message: `feedback queue full; dropped the ${over} oldest events`,
|
|
591
|
+
count: over
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
ensureTimer();
|
|
595
|
+
if (queue.length >= batchSize) void flush();
|
|
596
|
+
}
|
|
597
|
+
const takeBatch = (limit) => {
|
|
598
|
+
let bytes = BATCH_ENVELOPE_BYTES;
|
|
599
|
+
let n = 0;
|
|
600
|
+
while (n < queue.length && n < limit) {
|
|
601
|
+
const size = Buffer.byteLength(JSON.stringify(queue[n])) + 1;
|
|
602
|
+
if (n > 0 && bytes + size > MAX_BATCH_BYTES) break;
|
|
603
|
+
bytes += size;
|
|
604
|
+
n++;
|
|
605
|
+
}
|
|
606
|
+
const batch = queue.slice(0, n);
|
|
607
|
+
queue = queue.slice(n);
|
|
608
|
+
return batch;
|
|
609
|
+
};
|
|
610
|
+
async function drain() {
|
|
611
|
+
let limit = batchSize;
|
|
612
|
+
while (queue.length > 0) {
|
|
613
|
+
const batch = takeBatch(limit);
|
|
614
|
+
const res = await postWithRetry(
|
|
615
|
+
`${endpoint}/v1/feedback`,
|
|
616
|
+
projectKey,
|
|
617
|
+
{ events: batch },
|
|
618
|
+
options
|
|
619
|
+
);
|
|
620
|
+
if (res.ok) continue;
|
|
621
|
+
if (res.status === 413 && batch.length > 1) {
|
|
622
|
+
limit = Math.max(1, Math.floor(batch.length / 2));
|
|
623
|
+
queue = [...batch, ...queue];
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (res.retryable) {
|
|
627
|
+
queue = [...batch, ...queue].slice(0, maxQueue);
|
|
628
|
+
report({ kind: "feedback", message: res.error, status: res.status });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const bad = invalidIndexes(res, batch.length);
|
|
632
|
+
if (bad && bad.size < batch.length) {
|
|
633
|
+
report({
|
|
634
|
+
kind: "dropped",
|
|
635
|
+
message: `feedback refused ${bad.size} invalid event${bad.size === 1 ? "" : "s"}: ${res.error}`,
|
|
636
|
+
status: res.status,
|
|
637
|
+
count: bad.size
|
|
638
|
+
});
|
|
639
|
+
queue = [...batch.filter((_, i) => !bad.has(i)), ...queue];
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
report({
|
|
643
|
+
kind: "dropped",
|
|
644
|
+
message: `feedback batch refused (${res.status}): ${res.error}`,
|
|
645
|
+
status: res.status,
|
|
646
|
+
count: batch.length
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function flush() {
|
|
651
|
+
if (!enabled) return Promise.resolve();
|
|
652
|
+
if (!inFlight) {
|
|
653
|
+
const run = drain().finally(() => {
|
|
654
|
+
if (inFlight === run) inFlight = null;
|
|
655
|
+
});
|
|
656
|
+
inFlight = run;
|
|
657
|
+
return run;
|
|
658
|
+
}
|
|
659
|
+
if (!followUp) {
|
|
660
|
+
const next = inFlight.then(() => {
|
|
661
|
+
if (followUp === next) followUp = null;
|
|
662
|
+
return flush();
|
|
663
|
+
});
|
|
664
|
+
followUp = next;
|
|
665
|
+
}
|
|
666
|
+
return followUp;
|
|
667
|
+
}
|
|
668
|
+
async function shutdown() {
|
|
669
|
+
closed = true;
|
|
670
|
+
if (timer) clearInterval(timer);
|
|
671
|
+
timer = null;
|
|
672
|
+
await flush();
|
|
673
|
+
if (queue.length > 0)
|
|
674
|
+
report({
|
|
675
|
+
kind: "dropped",
|
|
676
|
+
message: `shutdown with ${queue.length} unsent feedback events`,
|
|
677
|
+
count: queue.length
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
enabled,
|
|
682
|
+
registerAgentVersion,
|
|
683
|
+
instrumentation: (opts) => createInstrumentation((name) => registry.get(name), opts),
|
|
684
|
+
feedback: (event) => enqueue([event]),
|
|
685
|
+
feedbackBatch: (events) => enqueue(events),
|
|
686
|
+
flush,
|
|
687
|
+
shutdown,
|
|
688
|
+
versions: () => registry
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
export {
|
|
692
|
+
ATTR_COST_USD,
|
|
693
|
+
ATTR_SUBJECT_VERSION_ID,
|
|
694
|
+
ATTR_VERSION_HASH,
|
|
695
|
+
EVAL_STUDIO_INSTRUMENTATION_KEY,
|
|
696
|
+
FEEDBACK_TYPES,
|
|
697
|
+
REASON_CODES,
|
|
698
|
+
agentVersion,
|
|
699
|
+
agentVersionHash,
|
|
700
|
+
backoffDelay,
|
|
701
|
+
createEvalStudio,
|
|
702
|
+
createInstrumentation,
|
|
703
|
+
editStats,
|
|
704
|
+
hashActor,
|
|
705
|
+
postWithRetry,
|
|
706
|
+
toWireEvent,
|
|
707
|
+
toolManifestEntry
|
|
708
|
+
};
|