@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.
@@ -0,0 +1,180 @@
1
+ import { FlueObservation } from '@flue/runtime';
2
+ import { PushEvent, IngestPushBody, FeedbackBody } from '@repo/schema/ingest';
3
+ import { AgentSubject, DeclaredScope, AgentBom, JsonValue, SubjectVersionRegistration } from '@repo/schema/stamping';
4
+ export { AgentSubject, ToolManifestEntry, instructionsHash, subjectVersionHash, toolManifestHash } from '@repo/schema/stamping';
5
+ import { T as ToolLike } from './version-DU6_w67Z.js';
6
+ export { t as toolManifestEntry } from './version-DU6_w67Z.js';
7
+
8
+ /** A declared subject plus the optional provenance Studio stores beside it. */
9
+ type DeclaredSubject = AgentSubject & {
10
+ /** The instruction text behind instructionsHash (shown in Studio's spec sheet). */
11
+ instructions?: string;
12
+ /** The tool names in the manifest. */
13
+ toolNames?: string[];
14
+ /**
15
+ * Declared instance-id scope (#77): a named conversation-id template with a
16
+ * `{case}` slot that Eval Studio fills with its eval run id when an
17
+ * experiment targets this scope. One subject per scope.
18
+ */
19
+ scope?: DeclaredScope;
20
+ /** The tools as the spec sheet renders them (a partial BOM: tools only). */
21
+ bom?: AgentBom;
22
+ };
23
+
24
+ /**
25
+ * Build an agent's subject from what it renders: its system instructions,
26
+ * the tools it mounts (Flue ToolDefinitions from defineTool) and its model.
27
+ *
28
+ * agentSubject({ agentName: "hoa-v2", instructions: HOA_V2_INSTRUCTIONS,
29
+ * tools: [lookupGoverningDocs, checkAmendments], model: HOA_MODEL })
30
+ */
31
+ declare function agentSubject(args: {
32
+ agentName: string;
33
+ instructions: string;
34
+ tools: ToolLike[];
35
+ model: string;
36
+ scope?: DeclaredScope;
37
+ }): DeclaredSubject;
38
+ /** Valid registrations for the declared subjects; invalid ones are reported and skipped. */
39
+ declare function subjectRegistrations(subjects: DeclaredSubject[]): SubjectVersionRegistration[];
40
+ /**
41
+ * The URL of one agent conversation on this app: base + agent mount + id.
42
+ * `agentPath` is a template with an `{agent}` slot (Flue 2 apps mount their
43
+ * agents themselves; `/agents/{agent}` is the reference app's convention).
44
+ */
45
+ declare function conversationUrl(baseUrl: string, agentPath: string, agentName: string, instanceId: string): string;
46
+ type EvalStudioReporterOptions = {
47
+ /**
48
+ * Agent versions to register at boot — one per agent (or per declared
49
+ * scope). Build each with agentSubject(). Absent → nothing is stamped and
50
+ * runs land unversioned.
51
+ */
52
+ subjects?: DeclaredSubject[];
53
+ /** Eval Studio base URL. Default: EVAL_STUDIO_URL. Absent → reporter is inert. */
54
+ url?: string;
55
+ /**
56
+ * Eval Studio project this app reports into (#18). Default:
57
+ * EVAL_STUDIO_PROJECT. Unknown names auto-create; absent → the default project.
58
+ */
59
+ project?: string;
60
+ /**
61
+ * Environment this app runs in ('prod' | 'staging' | 'dev' | …, free text).
62
+ * Default: EVAL_STUDIO_ENV, else UNSET — and Studio treats an unlabeled
63
+ * reporter as 'prod'. Declare it on pre-prod apps to keep their traffic out
64
+ * of monitoring (live judging, signals, dispatch, fleet health). Version
65
+ * stamps are deliberately env-less — the same version hash links pre-prod
66
+ * experiments and prod monitoring.
67
+ */
68
+ environment?: string;
69
+ /**
70
+ * THIS Flue app's own base URL. The reporter reads its LOCAL durable
71
+ * history from here for the push backbone (#43), and Studio records it as
72
+ * the app's location (and drives fresh evaluations against it). Default:
73
+ * FLUE_SELF_URL, else Vite's dev default http://127.0.0.1:5173.
74
+ */
75
+ selfUrl?: string;
76
+ /**
77
+ * Where agents are mounted under selfUrl, with an `{agent}` slot. Default:
78
+ * FLUE_AGENT_PATH, else `/agents/{agent}`. Studio must use the same shape
79
+ * (its own FLUE_AGENT_PATH) to address conversations.
80
+ */
81
+ agentPath?: string;
82
+ /**
83
+ * Project API key (Settings → API keys, `ingest` scope), sent as
84
+ * `Authorization: Bearer <key>`. It decides the project the app reports
85
+ * into. Default: EVAL_STUDIO_API_KEY.
86
+ */
87
+ apiKey?: string;
88
+ /**
89
+ * DEPRECATED legacy shared secret, sent as x-eval-studio-secret only when no
90
+ * apiKey is set; accepted only while the server sets
91
+ * EVAL_STUDIO_LEGACY_SECRET. Default: EVAL_STUDIO_LEGACY_SECRET.
92
+ */
93
+ secret?: string;
94
+ /**
95
+ * Commit SHA the app runs from (#69) — recorded on each activation AND
96
+ * pinned on the SubjectVersion it mints, powering Studio's "View source
97
+ * @ sha" deep link. Default: GIT_SHA (set it in deploy pipelines), else a
98
+ * .git read at boot (dev / source checkouts). No .git → stamps go unpinned.
99
+ */
100
+ gitRef?: string;
101
+ /**
102
+ * Where the agent's source can be browsed (#69) — composed with the commit
103
+ * SHA into the spec sheet's deep link. Default: EVAL_STUDIO_REPO_URL, else
104
+ * derived from the checkout's git remote origin (ssh forms normalize to
105
+ * https). Absent → no deep link, everything else works.
106
+ */
107
+ repoUrl?: string;
108
+ /**
109
+ * Content policy (#43): "full" shares message/event text; "off" keeps only
110
+ * structure + usage (Sentry's default). Image bytes are ALWAYS replaced with
111
+ * the IMAGE_DATA_OMITTED sentinel regardless. Default: EVAL_STUDIO_CONTENT_MODE
112
+ * ("off" iff set to "off"), else "full".
113
+ */
114
+ contentMode?: "full" | "off";
115
+ /**
116
+ * Last-mile redaction hook — runs on every event just before it is pushed
117
+ * (after image-scrub + content-mode). Return the event to keep it, or the
118
+ * mutated copy; the source decides what leaves the building.
119
+ */
120
+ redact?: (event: PushEvent) => PushEvent;
121
+ /**
122
+ * Sampling for high-volume, non-load-bearing events (tool / task / log):
123
+ * 0..1, fraction KEPT. turn / turn_request / submission_settled / compaction
124
+ * are never sampled out — cost, the composed request, and the outcome must
125
+ * always survive. Default 1 (keep everything).
126
+ */
127
+ eventSampleRate?: number;
128
+ };
129
+ /**
130
+ * The source-side content policy, applied to a backbone or an event before it
131
+ * leaves the app. One recursive pass: image bytes become IMAGE_DATA_OMITTED
132
+ * (any object carrying a `mimeType` has its `data` replaced — Flue's
133
+ * {type:'image',data,mimeType} block), content-off blanks every string under
134
+ * a content key, and the result is coerced to a serializable JSON value
135
+ * (functions/undefined are dropped, so a push body never fails to stringify).
136
+ */
137
+ declare function sharePolicy(value: unknown, contentOff: boolean): JsonValue;
138
+ /** What the push observer needs from its host (injected, so tests can drive it). */
139
+ type PushObserverDeps = {
140
+ contentOff: boolean;
141
+ /** 0..1, fraction of tool / task / log events kept. */
142
+ sampleRate: number;
143
+ redact?: (event: PushEvent) => PushEvent;
144
+ /** Reads the conversation's local history snapshot; null when unreadable. */
145
+ readHistory: (agentName: string, instanceId: string) => Promise<unknown>;
146
+ push: (body: IngestPushBody) => Promise<void>;
147
+ /** Fields every push carries (project, environment, flueBaseUrl, gitRef). */
148
+ context: Pick<IngestPushBody, "project" | "environment" | "flueBaseUrl" | "gitRef">;
149
+ /** Grace before reading history, so the durable store has the turn. Default 500. */
150
+ settleDelayMs?: number;
151
+ };
152
+ /**
153
+ * The push seam's observer (#43): buffers the forwardable events per
154
+ * conversation and, on every submission_settled, drains that submission's
155
+ * events and pushes them with the local backbone. Exported for tests;
156
+ * initEvalStudioReporter wires it to observe().
157
+ */
158
+ declare function createPushObserver(deps: PushObserverDeps): {
159
+ observe: (event: FlueObservation) => void;
160
+ /** Events still buffered, across every conversation. */
161
+ buffered: () => number;
162
+ };
163
+ /**
164
+ * @deprecated Use `createEvalStudio()` from `@eval-studio/flue` with
165
+ * @flue/opentelemetry: spans for runs, registerAgentVersion() for versions,
166
+ * feedback() for human signals. This backbone push is removed in the next
167
+ * major.
168
+ */
169
+ declare function initEvalStudioReporter(options?: EvalStudioReporterOptions): void;
170
+ type ReportFeedbackOptions = {
171
+ /** Eval Studio base URL. Default: EVAL_STUDIO_URL. Absent → this call is inert. */
172
+ url?: string;
173
+ /** Project API key, sent as a bearer token. Default: EVAL_STUDIO_API_KEY. */
174
+ apiKey?: string;
175
+ /** DEPRECATED legacy secret (see initEvalStudioReporter). */
176
+ secret?: string;
177
+ };
178
+ declare function reportFeedback(feedback: FeedbackBody, options?: ReportFeedbackOptions): Promise<void>;
179
+
180
+ export { type DeclaredSubject, type EvalStudioReporterOptions, type PushObserverDeps, type ReportFeedbackOptions, ToolLike, agentSubject, conversationUrl, createPushObserver, initEvalStudioReporter, reportFeedback, sharePolicy, subjectRegistrations };
package/dist/legacy.js ADDED
@@ -0,0 +1,449 @@
1
+ import {
2
+ instructionsHash,
3
+ isValidScopeTemplate,
4
+ manifestBom,
5
+ subjectProblem,
6
+ subjectRegistration,
7
+ subjectVersionHash,
8
+ toolManifestEntry,
9
+ toolManifestHash
10
+ } from "./chunk-LKO4T54N.js";
11
+
12
+ // src/legacy.ts
13
+ import { readFileSync, statSync } from "fs";
14
+ import { dirname, isAbsolute, join, resolve } from "path";
15
+ import { IMAGE_DATA_OMITTED, observe } from "@flue/runtime";
16
+ function findGitDir(start) {
17
+ let dir = start;
18
+ for (; ; ) {
19
+ const candidate = join(dir, ".git");
20
+ try {
21
+ const stat = statSync(candidate);
22
+ if (stat.isDirectory()) return candidate;
23
+ if (stat.isFile()) {
24
+ const m = /^gitdir:\s*(.+)\s*$/m.exec(readFileSync(candidate, "utf8"));
25
+ if (m?.[1]) return isAbsolute(m[1]) ? m[1] : resolve(dir, m[1]);
26
+ return null;
27
+ }
28
+ } catch {
29
+ }
30
+ const parent = dirname(dir);
31
+ if (parent === dir) return null;
32
+ dir = parent;
33
+ }
34
+ }
35
+ function readGitSha(gitDir) {
36
+ try {
37
+ const head = readFileSync(join(gitDir, "HEAD"), "utf8").trim();
38
+ const symref = /^ref:\s*(.+)$/.exec(head)?.[1]?.trim();
39
+ if (!symref) return /^[0-9a-f]{40}$/i.test(head) ? head : void 0;
40
+ let common = gitDir;
41
+ try {
42
+ const c = readFileSync(join(gitDir, "commondir"), "utf8").trim();
43
+ common = isAbsolute(c) ? c : resolve(gitDir, c);
44
+ } catch {
45
+ }
46
+ try {
47
+ const sha = readFileSync(join(common, symref), "utf8").trim();
48
+ if (/^[0-9a-f]{40}$/i.test(sha)) return sha;
49
+ } catch {
50
+ }
51
+ const packed = readFileSync(join(common, "packed-refs"), "utf8");
52
+ for (const line of packed.split("\n")) {
53
+ if (line.endsWith(` ${symref}`)) return line.slice(0, 40);
54
+ }
55
+ } catch {
56
+ }
57
+ return void 0;
58
+ }
59
+ function readRepoUrl(gitDir) {
60
+ try {
61
+ const config = readFileSync(join(gitDir, "config"), "utf8");
62
+ const section = /\[remote "origin"\][^[]*/.exec(config)?.[0];
63
+ const raw = section && /^\s*url\s*=\s*(.+)\s*$/m.exec(section)?.[1]?.trim();
64
+ if (!raw) return void 0;
65
+ const bare = raw.replace(/\.git$/, "");
66
+ if (/^https?:\/\//.test(bare)) return bare;
67
+ const ssh = /^ssh:\/\/(?:[^@]+@)?([^:/]+)(?::\d+)?\/(.+)$/.exec(bare);
68
+ if (ssh) return `https://${ssh[1]}/${ssh[2]}`;
69
+ if (bare.includes("://")) return void 0;
70
+ const scp = /^(?:[^@]+@)?([^:/]+):(.+)$/.exec(bare);
71
+ if (scp) return `https://${scp[1]}/${scp[2]}`;
72
+ return void 0;
73
+ } catch {
74
+ return void 0;
75
+ }
76
+ }
77
+ function probeGit() {
78
+ try {
79
+ const gitDir = findGitDir(process.cwd());
80
+ if (!gitDir) return {};
81
+ return { sha: readGitSha(gitDir), repoUrl: readRepoUrl(gitDir) };
82
+ } catch {
83
+ return {};
84
+ }
85
+ }
86
+ function agentSubject(args) {
87
+ const manifest = args.tools.map(toolManifestEntry);
88
+ return {
89
+ agentName: args.agentName,
90
+ instructionsHash: instructionsHash(args.instructions),
91
+ toolManifestHash: toolManifestHash(manifest),
92
+ model: args.model,
93
+ instructions: args.instructions,
94
+ toolNames: args.tools.map((t) => t.name),
95
+ ...args.scope ? { scope: args.scope } : {},
96
+ bom: manifestBom(manifest)
97
+ };
98
+ }
99
+ function subjectRegistrations(subjects) {
100
+ const out = [];
101
+ for (const subject of subjects) {
102
+ const problem = subjectProblem(subject) ?? (subject.scope && !isValidScopeTemplate(subject.scope.template) ? `scope "${subject.scope.name}" template must contain "{case}"` : null);
103
+ if (problem) {
104
+ console.warn(
105
+ `[eval-studio] subject ${subject.agentName || "(unnamed)"} ignored: ${problem}`
106
+ );
107
+ continue;
108
+ }
109
+ out.push(subjectRegistration(subject));
110
+ }
111
+ return out;
112
+ }
113
+ function conversationUrl(baseUrl, agentPath, agentName, instanceId) {
114
+ const mount = agentPath.replaceAll("{agent}", encodeURIComponent(agentName)).replace(/\/$/, "");
115
+ return `${baseUrl.replace(/\/$/, "")}${mount}/${encodeURIComponent(instanceId)}`;
116
+ }
117
+ var BUFFERABLE = {
118
+ turn_request: "turn_request",
119
+ turn: "turn",
120
+ tool: "tool",
121
+ task: "task",
122
+ log: "log",
123
+ compaction: "compaction",
124
+ submission_settled: "submission_settled"
125
+ };
126
+ var NEVER_SAMPLE = /* @__PURE__ */ new Set([
127
+ "turn",
128
+ "turn_request",
129
+ "submission_settled",
130
+ "compaction"
131
+ ]);
132
+ var CONTENT_KEYS = /* @__PURE__ */ new Set([
133
+ // free text and model I/O
134
+ "text",
135
+ "message",
136
+ "thinking",
137
+ "content",
138
+ "input",
139
+ "output",
140
+ "result",
141
+ "args",
142
+ "arguments",
143
+ "prompt",
144
+ "systemPrompt",
145
+ "toolResults",
146
+ // errors: their message, details and dev hints quote the payload
147
+ "error",
148
+ "errorText",
149
+ "details",
150
+ // Flue 2 parts: data-* payloads, files
151
+ "data",
152
+ "filename",
153
+ "url",
154
+ // Flue 2 observation detail
155
+ "effectiveResult",
156
+ "agentInput",
157
+ "agentOutput",
158
+ "attributes"
159
+ ]);
160
+ var STRUCTURE_KEYS = /* @__PURE__ */ new Set([
161
+ "type",
162
+ "role",
163
+ "state",
164
+ "toolName",
165
+ "toolCallId",
166
+ "mimeType",
167
+ "mediaType"
168
+ ]);
169
+ var METADATA_KEPT = /* @__PURE__ */ new Set(["usage", "model"]);
170
+ var OMITTED = "[content omitted]";
171
+ function sharePolicy(value, contentOff) {
172
+ return share(value, contentOff, false, null);
173
+ }
174
+ function share(value, contentOff, inContent, key) {
175
+ if (value === null) return null;
176
+ if (Array.isArray(value))
177
+ return value.map((v) => share(v, contentOff, inContent, null));
178
+ if (typeof value === "object") {
179
+ const src = value;
180
+ const isImageBlock = typeof src.mimeType === "string";
181
+ const inMetadata = contentOff && !inContent && key === "metadata";
182
+ const out = {};
183
+ for (const [k, v] of Object.entries(src)) {
184
+ if (k === "data" && isImageBlock) {
185
+ out[k] = IMAGE_DATA_OMITTED;
186
+ continue;
187
+ }
188
+ const childContent = inContent || contentOff && CONTENT_KEYS.has(k) || inMetadata && !METADATA_KEPT.has(k);
189
+ out[k] = share(v, contentOff, childContent, k);
190
+ }
191
+ return out;
192
+ }
193
+ if (typeof value === "string") {
194
+ if (inContent && !(key != null && STRUCTURE_KEYS.has(key)))
195
+ return value.length > 0 ? OMITTED : "";
196
+ return value;
197
+ }
198
+ if (typeof value === "number" || typeof value === "boolean") return value;
199
+ return null;
200
+ }
201
+ var leafCost = (usage) => {
202
+ const total = usage?.cost?.total;
203
+ return typeof total === "number" ? total : void 0;
204
+ };
205
+ function toPushEvent(obs, contentOff) {
206
+ const type = BUFFERABLE[obs.type];
207
+ if (!type) return null;
208
+ const e = obs;
209
+ const runKey = e.instanceId;
210
+ if (!runKey) return null;
211
+ const costUsd = type === "turn" ? leafCost(e.response?.usage) : type === "compaction" ? leafCost(e.usage) : void 0;
212
+ return {
213
+ runKey,
214
+ submissionId: e.submissionId ?? "",
215
+ eventIndex: obs.eventIndex,
216
+ type,
217
+ ...e.turnId ? { turnId: e.turnId } : {},
218
+ timestamp: obs.timestamp,
219
+ ...costUsd != null ? { costUsd } : {},
220
+ payload: sharePolicy(obs, contentOff)
221
+ };
222
+ }
223
+ function createPushObserver(deps) {
224
+ const { contentOff, sampleRate, redact } = deps;
225
+ const MAX_BUFFERED_PER_KEY = 5e3;
226
+ const buffer = /* @__PURE__ */ new Map();
227
+ const drainEvents = (runKey, submissionId) => {
228
+ const all = buffer.get(runKey) ?? [];
229
+ const taken = [];
230
+ const kept = [];
231
+ for (const e of all) {
232
+ const mine = e.submissionId === submissionId || e.submissionId === "";
233
+ if (mine) taken.push(redact ? redact(e) : e);
234
+ else kept.push(e);
235
+ }
236
+ if (kept.length > 0) buffer.set(runKey, kept);
237
+ else buffer.delete(runKey);
238
+ return taken;
239
+ };
240
+ const pushSettled = (agentName, instanceId, submissionId) => setTimeout(() => {
241
+ void (async () => {
242
+ const events = drainEvents(instanceId, submissionId);
243
+ const snapshot = await deps.readHistory(agentName, instanceId);
244
+ if (!snapshot && events.length === 0) return;
245
+ const body = {
246
+ // No local backbone (read failed): send an empty snapshot so the
247
+ // events still ingest; a later settle re-reads and re-pushes it.
248
+ backbone: snapshot ? sharePolicy(snapshot, contentOff) : {
249
+ v: 1,
250
+ conversationId: instanceId,
251
+ offset: "",
252
+ messages: [],
253
+ settlements: []
254
+ },
255
+ events,
256
+ agentName,
257
+ instanceId,
258
+ submissionId,
259
+ ...deps.context,
260
+ contentMode: contentOff ? "off" : "full"
261
+ };
262
+ await deps.push(body);
263
+ })();
264
+ }, deps.settleDelayMs ?? 500);
265
+ const onEvent = (event) => {
266
+ const pushEvent = toPushEvent(event, contentOff);
267
+ if (pushEvent) {
268
+ const sampled = NEVER_SAMPLE.has(pushEvent.type) || sampleRate >= 1 || Math.random() < sampleRate;
269
+ if (sampled) {
270
+ const list = buffer.get(pushEvent.runKey) ?? [];
271
+ list.push(pushEvent);
272
+ if (list.length > MAX_BUFFERED_PER_KEY)
273
+ list.splice(0, list.length - MAX_BUFFERED_PER_KEY);
274
+ buffer.set(pushEvent.runKey, list);
275
+ }
276
+ }
277
+ if (event.type === "submission_settled") {
278
+ const { agentName, instanceId, submissionId } = event;
279
+ if (!agentName || !instanceId) return;
280
+ pushSettled(agentName, instanceId, submissionId);
281
+ }
282
+ };
283
+ return {
284
+ observe: onEvent,
285
+ buffered: () => [...buffer.values()].reduce((n, l) => n + l.length, 0)
286
+ };
287
+ }
288
+ var warnedDeprecated = false;
289
+ function initEvalStudioReporter(options = {}) {
290
+ if (!warnedDeprecated) {
291
+ warnedDeprecated = true;
292
+ console.warn(
293
+ "[eval-studio] initEvalStudioReporter() is deprecated: switch to createEvalStudio() from @eval-studio/flue (see its README)."
294
+ );
295
+ }
296
+ const url = (options.url ?? process.env.EVAL_STUDIO_URL)?.replace(/\/$/, "");
297
+ if (!url) return;
298
+ const project = options.project ?? process.env.EVAL_STUDIO_PROJECT;
299
+ const environment = options.environment ?? process.env.EVAL_STUDIO_ENV;
300
+ const selfUrl = (options.selfUrl ?? process.env.FLUE_SELF_URL ?? "http://127.0.0.1:5173").replace(/\/$/, "");
301
+ const agentPath = options.agentPath ?? process.env.FLUE_AGENT_PATH ?? "/agents/{agent}";
302
+ const needProbe = !(options.gitRef ?? process.env.GIT_SHA) || !(options.repoUrl ?? process.env.EVAL_STUDIO_REPO_URL);
303
+ const probed = needProbe ? probeGit() : {};
304
+ const gitRef = options.gitRef ?? process.env.GIT_SHA ?? probed.sha;
305
+ const repoUrl = (options.repoUrl ?? process.env.EVAL_STUDIO_REPO_URL ?? probed.repoUrl)?.replace(/\/$/, "");
306
+ const headers = {
307
+ "content-type": "application/json",
308
+ ...studioAuthHeaders(options)
309
+ };
310
+ const contentOff = (options.contentMode ?? process.env.EVAL_STUDIO_CONTENT_MODE) === "off";
311
+ const readLocalHistory = async (agentName, instanceId) => {
312
+ const target = conversationUrl(selfUrl, agentPath, agentName, instanceId);
313
+ try {
314
+ const res = await fetch(target, {
315
+ headers: { accept: "application/json" }
316
+ });
317
+ if (!res.ok) {
318
+ console.warn(
319
+ `[eval-studio] local history read for ${agentName}/${instanceId}: ${res.status}`
320
+ );
321
+ return null;
322
+ }
323
+ return await res.json();
324
+ } catch (err) {
325
+ console.warn(
326
+ `[eval-studio] local history read failed for ${agentName}/${instanceId}: ${err}`
327
+ );
328
+ return null;
329
+ }
330
+ };
331
+ const pushWithRetry = async (body) => {
332
+ const label = `${body.agentName}/${body.instanceId}${body.submissionId ? `/${body.submissionId}` : ""}`;
333
+ for (let attempt = 1; attempt <= 3; attempt++) {
334
+ try {
335
+ const res = await fetch(`${url}/api/ingest-push`, {
336
+ method: "POST",
337
+ headers,
338
+ body: JSON.stringify(body)
339
+ });
340
+ if (res.ok) return;
341
+ console.warn(
342
+ `[eval-studio] push ${label} rejected: ${res.status} ${await res.text()}`
343
+ );
344
+ if (res.status < 500) return;
345
+ } catch (err) {
346
+ console.warn(
347
+ `[eval-studio] push ${label} failed (attempt ${attempt}, backbone re-readable): ${err}`
348
+ );
349
+ }
350
+ if (attempt < 3) await new Promise((r) => setTimeout(r, 250 * attempt));
351
+ }
352
+ };
353
+ observe(
354
+ createPushObserver({
355
+ contentOff,
356
+ sampleRate: options.eventSampleRate ?? 1,
357
+ ...options.redact ? { redact: options.redact } : {},
358
+ readHistory: readLocalHistory,
359
+ push: pushWithRetry,
360
+ context: {
361
+ ...project ? { project } : {},
362
+ ...environment ? { environment } : {},
363
+ flueBaseUrl: selfUrl,
364
+ ...gitRef ? { gitRef } : {}
365
+ }
366
+ }).observe
367
+ );
368
+ const registrations = subjectRegistrations(options.subjects ?? []);
369
+ if (registrations.length === 0) return;
370
+ void fetch(`${url}/api/subject-versions`, {
371
+ method: "POST",
372
+ headers,
373
+ body: JSON.stringify({
374
+ registrations,
375
+ ...project ? { project } : {},
376
+ flueBaseUrl: selfUrl,
377
+ ...repoUrl ? { repoUrl } : {},
378
+ gitRef,
379
+ activatedAt: (/* @__PURE__ */ new Date()).toISOString()
380
+ })
381
+ }).then(async (res) => {
382
+ if (!res.ok)
383
+ console.warn(
384
+ `[eval-studio] version registration rejected: ${res.status} ${await res.text()}`
385
+ );
386
+ }).catch(
387
+ (e) => console.warn(
388
+ `[eval-studio] version registration failed (runs stay unversioned until the next boot): ${e}`
389
+ )
390
+ );
391
+ }
392
+ async function reportFeedback(feedback, options = {}) {
393
+ const url = (options.url ?? process.env.EVAL_STUDIO_URL)?.replace(/\/$/, "");
394
+ if (!url) return;
395
+ if (!feedback.conversationId || !feedback.submissionId) {
396
+ console.warn(
397
+ "[eval-studio] reportFeedback ignored: conversationId and submissionId are required"
398
+ );
399
+ return;
400
+ }
401
+ const headers = {
402
+ "content-type": "application/json",
403
+ ...studioAuthHeaders(options)
404
+ };
405
+ const label = `${feedback.conversationId}/${feedback.submissionId} (${feedback.verdict})`;
406
+ for (let attempt = 1; attempt <= 4; attempt++) {
407
+ try {
408
+ const res = await fetch(`${url}/api/feedback`, {
409
+ method: "POST",
410
+ headers,
411
+ body: JSON.stringify(feedback)
412
+ });
413
+ if (res.ok) return;
414
+ if (res.status !== 404 && res.status < 500) {
415
+ console.warn(
416
+ `[eval-studio] feedback ${label} rejected: ${res.status} ${await res.text()}`
417
+ );
418
+ return;
419
+ }
420
+ console.warn(
421
+ `[eval-studio] feedback ${label}: ${res.status} (attempt ${attempt}) \u2014 will retry`
422
+ );
423
+ } catch (err) {
424
+ console.warn(
425
+ `[eval-studio] feedback ${label} failed (attempt ${attempt}): ${err}`
426
+ );
427
+ }
428
+ if (attempt < 4) await new Promise((r) => setTimeout(r, 400 * attempt));
429
+ }
430
+ }
431
+ function studioAuthHeaders(options) {
432
+ const apiKey = options.apiKey ?? process.env.EVAL_STUDIO_API_KEY;
433
+ if (apiKey) return { authorization: `Bearer ${apiKey}` };
434
+ const secret = options.secret ?? process.env.EVAL_STUDIO_LEGACY_SECRET ?? process.env.EVAL_STUDIO_SECRET;
435
+ return secret ? { "x-eval-studio-secret": secret } : {};
436
+ }
437
+ export {
438
+ agentSubject,
439
+ conversationUrl,
440
+ createPushObserver,
441
+ initEvalStudioReporter,
442
+ instructionsHash,
443
+ reportFeedback,
444
+ sharePolicy,
445
+ subjectRegistrations,
446
+ subjectVersionHash,
447
+ toolManifestEntry,
448
+ toolManifestHash
449
+ };
@@ -0,0 +1,40 @@
1
+ import { ToolManifestEntry } from '@repo/schema/stamping';
2
+
3
+ /** The parts of a Flue tool definition (defineTool) the manifest reads. */
4
+ type ToolLike = {
5
+ name: string;
6
+ description: string;
7
+ /** A valibot schema (Flue's defineTool `input`). */
8
+ input?: unknown;
9
+ };
10
+ /**
11
+ * A tool's manifest entry: name, description and its input as JSON Schema
12
+ * (converted from valibot; an unconvertible part degrades instead of throwing).
13
+ */
14
+ declare function toolManifestEntry(tool: ToolLike): ToolManifestEntry;
15
+ /** What an agent renders: the inputs to its version identity. */
16
+ type AgentDefinition = {
17
+ /** The agent's durable name (its `agentName` static or function name). */
18
+ agentName: string;
19
+ /** The rendered system instructions (what the agent function returns). */
20
+ instructions: string;
21
+ /** The tools it mounts: defineTool() values or manifest entries. */
22
+ tools: readonly (ToolLike | ToolManifestEntry)[];
23
+ /** The model it passes to useModel(), verbatim. */
24
+ model: string;
25
+ };
26
+ /** One agent version: its digest and the parts it was computed from. */
27
+ type AgentVersion = {
28
+ agentName: string;
29
+ model: string;
30
+ instructionsHash: string;
31
+ toolsHash: string;
32
+ versionHash: string;
33
+ toolNames: string[];
34
+ };
35
+ /** Compute an agent's version identity. Pure and synchronous. */
36
+ declare function agentVersion(def: AgentDefinition): AgentVersion;
37
+ /** Just the digest — what eval.version_hash carries. */
38
+ declare const agentVersionHash: (def: AgentDefinition) => string;
39
+
40
+ export { type AgentDefinition as A, type ToolLike as T, type AgentVersion as a, agentVersion as b, agentVersionHash as c, toolManifestEntry as t };