@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 ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@eval-studio/flue`, released as tags of
4
+ [oghaAI/flue-eval-reporter](https://github.com/oghaAI/flue-eval-reporter).
5
+ Install a release with `bun add github:oghaAI/flue-eval-reporter#vX.Y.Z`.
6
+
7
+ ## 1.1.0
8
+
9
+ The first release of the Eval Studio SDK for Flue 2.3 apps, replacing the
10
+ v0.x backbone reporter. It works beside `@flue/opentelemetry`: runs reach
11
+ Eval Studio as OpenTelemetry spans over OTLP, and the SDK adds what spans
12
+ alone cannot carry. There is no 1.0.0 tag; 1.1.0 follows 0.4.0.
13
+
14
+ - **Agent versions.** `createEvalStudio()` and `registerAgentVersion()` hash
15
+ the agent name, rendered system instructions, tool manifest and model into
16
+ one version identity, register it with `POST /v1/subject-versions`, and
17
+ stamp every `invoke_agent` span with `eval.subject_version_id` and
18
+ `eval.version_hash`. The stamp is computed locally, so spans carry it from
19
+ the first request. `serviceVersion` (default `OTEL_SERVICE_VERSION`, else
20
+ `GIT_SHA`) is recorded beside each version. `sendInstructions: false`
21
+ keeps instruction text out; the hash still covers it.
22
+ - **Cost.** `evalStudio.instrumentation({ otel })` wraps Flue's OpenTelemetry
23
+ instrumentation and puts Flue's own per-call cost (`usage.cost.total`) on
24
+ each `chat` span as `flue.cost.usd`.
25
+ - **Human feedback.** `feedback()` reports approvals, rejections with a
26
+ reason code, revisions with their edit size, take-overs, hand-backs and
27
+ holds to `POST /v1/feedback`. Events are validated locally, batched (cut by
28
+ count and size, split on a `413`), retried with backoff on network errors,
29
+ `408`, `429` and `5xx`, and kept queued through an outage, up to
30
+ `maxQueueSize` (default 10 000; the oldest are dropped past it and reported
31
+ to `onError`). One bad event no longer sinks its batch. `hashActor()` and `editStats()` keep reviewer ids
32
+ and edited text in your process; note text is never sent.
33
+ - **Inert without configuration.** With no `EVAL_STUDIO_URL` /
34
+ `EVAL_STUDIO_PROJECT_KEY` nothing is sent; versions are still hashed and
35
+ stamped.
36
+ - **Peers:** `@flue/runtime` ^2.3, `@opentelemetry/api` ^1.9, `valibot` ^1.1.
37
+ `@valibot/to-json-schema` is the one regular dependency.
38
+ - **Deprecated:** the v0.x push reporter (`initEvalStudioReporter`,
39
+ `agentSubject`, `reportFeedback`) still ships at `@eval-studio/flue/legacy`
40
+ for apps mid-migration. See the README's migration section.
package/README.md CHANGED
@@ -1,3 +1,255 @@
1
- # Temporary Holding Version
1
+ # @eval-studio/flue
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Eval Studio SDK for Flue 2.3 apps. It works beside
4
+ [`@flue/opentelemetry`](https://flueframework.com/docs/ecosystem/tooling/opentelemetry/),
5
+ which exports the spans Eval Studio turns into runs, and adds the three things
6
+ spans alone cannot carry:
7
+
8
+ - **Agent version.** `registerAgentVersion()` hashes the agent name, the
9
+ rendered system instructions, the tool manifest and the model into one
10
+ version identity, registers it with Eval Studio, and every `invoke_agent`
11
+ span is stamped with it (`eval.subject_version_id`, `eval.version_hash`).
12
+ Each run in Eval Studio then names the exact version it ran.
13
+ - **Cost.** Flue knows what each model call cost (`usage.cost.total` from its
14
+ model catalog). The SDK puts that number on each `chat` span as
15
+ `flue.cost.usd`. Eval Studio uses it before falling back to its own token ×
16
+ price table.
17
+ - **Human feedback.** `feedback()` reports what a reviewer did with an agent's
18
+ output: approved, rejected (with a reason code), revised (with how much was
19
+ edited), taken over, handed back. Events are batched and retried.
20
+
21
+ The SDK never creates spans, a tracer provider or an exporter. Your
22
+ OpenTelemetry setup stays as it is.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ bun add github:oghaAI/flue-eval-reporter#v1.1.0
28
+ # or: npm install github:oghaAI/flue-eval-reporter#v1.1.0
29
+ ```
30
+
31
+ The package installs as `@eval-studio/flue`. Peer dependencies, which a Flue
32
+ app with OpenTelemetry already has: `@flue/runtime` ^2.3, `@opentelemetry/api`
33
+ ^1.9, `valibot` ^1.1. Its one regular dependency, `@valibot/to-json-schema`
34
+ (which turns a tool's valibot input into the JSON Schema the tools hash
35
+ covers), installs with it and uses your `valibot`. Everything else, including
36
+ the shared hashing and validation code, is bundled into `dist/`. Always pin a
37
+ `#vX.Y.Z` tag.
38
+
39
+ ## Setup
40
+
41
+ You need a project key with the `ingest` scope (Eval Studio → **Settings → API
42
+ keys**). The key decides the project; nothing in a payload can change it.
43
+
44
+ ```ts title="src/telemetry.ts"
45
+ import { createEvalStudio } from "@eval-studio/flue"
46
+ import { createOpenTelemetryInstrumentation } from "@flue/opentelemetry"
47
+ import { instrument } from "@flue/runtime"
48
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"
49
+ import { NodeSDK } from "@opentelemetry/sdk-node"
50
+
51
+ import { SUPPORT_INSTRUCTIONS, lookupOrder, refund } from "./agents/support.ts"
52
+
53
+ new NodeSDK({ traceExporter: new OTLPTraceExporter() }).start()
54
+
55
+ export const evalStudio = createEvalStudio({
56
+ endpoint: process.env.EVAL_STUDIO_URL, // https://evals.example.com
57
+ projectKey: process.env.EVAL_STUDIO_PROJECT_KEY, // es_ingest_…
58
+ serviceVersion: process.env.GIT_SHA, // recorded beside each version
59
+ })
60
+ instrument(
61
+ evalStudio.instrumentation({ otel: createOpenTelemetryInstrumentation() })
62
+ )
63
+
64
+ void evalStudio.registerAgentVersion({
65
+ agentName: "support",
66
+ instructions: SUPPORT_INSTRUCTIONS, // what the agent function returns
67
+ tools: [lookupOrder, refund], // the defineTool() values it mounts
68
+ model: "openrouter/deepseek/deepseek-v4-flash", // what it passes to useModel()
69
+ })
70
+ ```
71
+
72
+ ```ts title="src/app.ts"
73
+ import "./telemetry.ts" // first, before anything that makes model calls
74
+ ```
75
+
76
+ Point the OpenTelemetry exporter at the same Eval Studio with the same key:
77
+
78
+ ```sh
79
+ OTEL_EXPORTER_OTLP_ENDPOINT=https://evals.example.com
80
+ OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer es_ingest_…"
81
+ OTEL_SERVICE_NAME=support-app
82
+ ```
83
+
84
+ Things to know:
85
+
86
+ - **Install one instrumentation.** `evalStudio.instrumentation({ otel })`
87
+ wraps the OpenTelemetry instrumentation; install it **instead of**
88
+ `createOpenTelemetryInstrumentation()`, not beside it. The wrapping puts the
89
+ version stamp inside the agent span and lets the cost bridge set
90
+ `flue.cost.usd` before the chat span ends. Installed on its own (after the
91
+ OpenTelemetry one) it still stamps versions, but cannot bridge cost.
92
+ - **Register at boot.** The stamp is computed locally, so spans carry
93
+ `eval.version_hash` from the first request, even before Eval Studio has
94
+ answered. Eval Studio resolves the version by hash in that case.
95
+ - **Keep the definition in step with the agent.** The definition is what the
96
+ agent renders. If it drifts, you register a version the agent is not
97
+ running.
98
+ - **Inert without configuration.** With no `endpoint` or `projectKey` (and no
99
+ `EVAL_STUDIO_URL` / `EVAL_STUDIO_PROJECT_KEY` in the environment) nothing is
100
+ sent. Versions are still hashed and stamped. It is safe to leave in
101
+ production code paths.
102
+ - **Flush on shutdown.** Call `await evalStudio.shutdown()` from your
103
+ shutdown path, next to your OpenTelemetry SDK's `shutdown()`.
104
+
105
+ `apps/flue-test` in the eval-studio repo is a complete working example
106
+ (`src/telemetry.ts`, `src/eval-studio.ts`, `scripts/send-feedback.ts`).
107
+
108
+ ## Version identity
109
+
110
+ ```text
111
+ instructions_hash = sha256(instructions), 12 hex
112
+ tools_hash = sha256(tools sorted by name, JSON Schema keys sorted), 12 hex
113
+ version_hash = sha256({ agentName, instructionsHash, model, toolManifestHash }), 12 hex
114
+ ```
115
+
116
+ The order you list tools in never changes the hash. A renamed tool, a reworded
117
+ description, a changed input schema, a different model or one changed word in
118
+ the instructions does. `agentVersion(def)` and `agentVersionHash(def)` compute
119
+ it without registering. Eval Studio recomputes `version_hash` from the parts
120
+ and refuses a mismatch.
121
+
122
+ The SDK sends the instruction text with the registration, so Eval Studio's
123
+ spec sheet can show it. Eval Studio stores it only when the project's
124
+ telemetry content mode is `redacted` or `full` (new projects default to
125
+ `none`, which keeps the hash and drops the text), and only when it hashes to
126
+ `instructions_hash`. Pass `sendInstructions: false` to keep it in the
127
+ process either way; the hashes are still sent.
128
+
129
+ ## Human feedback
130
+
131
+ ```ts
132
+ import { editStats, hashActor } from "@eval-studio/flue"
133
+
134
+ const salt = process.env.FEEDBACK_SALT! // one secret per deployment
135
+
136
+ // approved as is
137
+ evalStudio.feedback({
138
+ type: "approval.approved",
139
+ conversationId: caseId, // your conversation (Runbook: the case id)
140
+ submissionId, // Flue's submission id: links the event to its run
141
+ actorHash: hashActor(reviewer.id, salt),
142
+ decisionLatencyMs: Date.now() - shownAt,
143
+ })
144
+
145
+ // rejected: a reason code is required
146
+ evalStudio.feedback({
147
+ type: "approval.rejected",
148
+ conversationId: caseId,
149
+ submissionId,
150
+ reasonCode: "wrong_facts", // wrong_facts | wrong_action | tone | policy | duplicate | other
151
+ notePresent: note.length > 0, // the note itself is never sent
152
+ actorHash: hashActor(reviewer.id, salt),
153
+ })
154
+
155
+ // revised before sending: send the size of the edit, not the text
156
+ evalStudio.feedback({
157
+ type: "approval.revised",
158
+ conversationId: caseId,
159
+ submissionId,
160
+ edit: editStats(agentDraft, sentText), // { charsBefore, charsAfter, editDistance, editRatio }
161
+ })
162
+
163
+ // several at once
164
+ evalStudio.feedbackBatch([
165
+ { type: "case.takeover", conversationId: caseId, actorHash },
166
+ { type: "case.handback", conversationId: caseId, actorHash },
167
+ ])
168
+ ```
169
+
170
+ Event types: `approval.approved`, `approval.rejected`, `approval.revised`,
171
+ `case.takeover`, `case.handback`, `case.hold`, `edit.sent`. Optional fields:
172
+ `id` (a UUID is generated; it is the idempotency key, so set your own when you
173
+ may resend), `occurredAt`, `toolCallId`, `outputId`, `traceId`, `spanId`,
174
+ `agentName`, `actionType`, `decisionLatencyMs`, and `attributes` (a flat map of
175
+ up to 32 string, number or boolean values).
176
+
177
+ Eval Studio links each event to its run by `submissionId`, or by `traceId` /
178
+ `spanId` when you have those instead. Feedback that arrives before its run is
179
+ linked once the run's spans land.
180
+
181
+ `feedback()` validates the event and throws a `TypeError` on a mistake the
182
+ server would refuse, such as a rejection with no reason or a raw id in
183
+ `actorHash`. Valid events are queued and sent in batches (every
184
+ `flushIntervalMs`, default 2 s, or as soon as `batchSize`, default 100, are
185
+ waiting). `await evalStudio.flush()` sends what is queued now.
186
+
187
+ ### Privacy
188
+
189
+ - **Never send note text.** Report `notePresent: true`. The API has no field
190
+ for the note and refuses events that carry `note`, `comment` or similar.
191
+ - **Never send raw user ids or emails.** Use `hashActor(id, salt)`: a salted
192
+ sha256 that counts distinct reviewers without saying who they are. Use one
193
+ secret salt per deployment, not per event, so the same person hashes the
194
+ same way. The API only accepts a 64-hex digest in `actor_hash`.
195
+ - **Edits travel as numbers.** `editStats(before, after)` runs in your process
196
+ and returns four numbers; neither text leaves it.
197
+
198
+ ## Delivery
199
+
200
+ - Requests are retried with exponential backoff (500 ms doubling, with
201
+ jitter, capped at 30 s, 5 attempts) on network errors, `408`, `429` and
202
+ `5xx`. A `Retry-After` header is honoured.
203
+ - Other `4xx` answers (an invalid event, a bad key, a body over the cap) are
204
+ not retried. The batch is dropped and reported to `onError`.
205
+ - If a batch still fails after every retry, it stays queued for the next
206
+ flush. The queue holds up to `maxQueueSize` events (default 10 000); past
207
+ that the oldest are dropped and reported.
208
+ - `shutdown()` stops the timer, flushes, and refuses further events.
209
+
210
+ | Option | Default |
211
+ | ------------------------------------------ | ---------------------------------------------------- |
212
+ | `endpoint` | `EVAL_STUDIO_URL` |
213
+ | `projectKey` | `EVAL_STUDIO_PROJECT_KEY` (or `EVAL_STUDIO_API_KEY`) |
214
+ | `serviceVersion` | `OTEL_SERVICE_VERSION`, else `GIT_SHA` |
215
+ | `batchSize` | 100 (server cap 500) |
216
+ | `flushIntervalMs` | 2000 |
217
+ | `maxQueueSize` | 10 000 |
218
+ | `maxAttempts`, `baseDelayMs`, `maxDelayMs` | 5, 500, 30 000 |
219
+ | `timeoutMs` | 10 000 per attempt |
220
+ | `sendInstructions` | `true` |
221
+ | `onError` | logs a warning |
222
+
223
+ ## API reference
224
+
225
+ The two endpoints the SDK calls, `POST /v1/subject-versions` and
226
+ `POST /v1/feedback`, are documented in Eval Studio's
227
+ `docs/sdk/api-reference.md`, for apps that send without this SDK.
228
+
229
+ ## Migrating from the reporter (`initEvalStudioReporter`)
230
+
231
+ The backbone push reporter still ships, deprecated, at
232
+ `@eval-studio/flue/legacy`. Eval Studio still serves `/api/ingest-push`, and
233
+ answers a beta.9 snapshot with `422`. To move:
234
+
235
+ 1. Set up `@flue/opentelemetry` with an OTLP exporter as above. Runs now come
236
+ from spans.
237
+ 2. Replace `initEvalStudioReporter({ subjects })` with `createEvalStudio()` and
238
+ one `registerAgentVersion()` per agent. The object you passed to
239
+ `agentSubject()` is the definition `registerAgentVersion()` takes, and the
240
+ hashes are identical, so existing versions are re-used, not duplicated.
241
+ 3. Replace `reportFeedback({ verdict })` with `feedback({ type })`.
242
+
243
+ ## Releasing (maintainers)
244
+
245
+ ```sh
246
+ packages/flue-eval-reporter/scripts/release.sh 1.1.0 # prepare: build, stage, commit, tag locally
247
+ packages/flue-eval-reporter/scripts/release.sh 1.1.0 --push # then publish the prepared tag
248
+ ```
249
+
250
+ The script builds with `tsup` (ESM + types, the shared `@repo/schema` slices
251
+ inlined, `@flue/runtime` and `@opentelemetry/api` external) and lays down
252
+ `package.json` (name `@eval-studio/flue`), `dist/` and this README in a clone
253
+ of the `oghaAI/flue-eval-reporter` dist repo, tagged `v<version>`. Without
254
+ `--push` it stops before pushing and prints the push command. The source of
255
+ truth stays in the eval-studio monorepo; never edit the dist repo by hand.
@@ -0,0 +1,131 @@
1
+ // ../schema/src/stamping.ts
2
+ import { createHash } from "crypto";
3
+ var sha12 = (s) => createHash("sha256").update(s).digest("hex").slice(0, 12);
4
+ function promptHash(instructions) {
5
+ return sha12(instructions);
6
+ }
7
+ function instructionsHash(instructions) {
8
+ return promptHash(instructions);
9
+ }
10
+ function toolManifestHash(tools) {
11
+ const canonical = [...tools].map((t) => ({
12
+ name: t.name,
13
+ description: t.description,
14
+ ...t.inputSchema !== void 0 ? { inputSchema: t.inputSchema } : {}
15
+ })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
16
+ return sha12(stableStringify(canonical));
17
+ }
18
+ var HASH12 = /^[0-9a-f]{12}$/;
19
+ function subjectProblem(subject) {
20
+ if (!subject.agentName.trim()) return "agentName is empty";
21
+ if (!HASH12.test(subject.instructionsHash))
22
+ return "instructionsHash must be 12 lowercase hex characters";
23
+ if (!HASH12.test(subject.toolManifestHash))
24
+ return "toolManifestHash must be 12 lowercase hex characters";
25
+ if (typeof subject.model !== "string") return "model must be a string";
26
+ return null;
27
+ }
28
+ function subjectVersionHash(subject) {
29
+ return sha12(
30
+ stableStringify({
31
+ agentName: subject.agentName,
32
+ instructionsHash: subject.instructionsHash,
33
+ model: subject.model,
34
+ toolManifestHash: subject.toolManifestHash
35
+ })
36
+ );
37
+ }
38
+ function subjectRegistration(subject) {
39
+ return {
40
+ agentName: subject.agentName,
41
+ promptHash: subject.instructionsHash,
42
+ skillHash: subject.toolManifestHash,
43
+ model: subject.model,
44
+ ...subject.instructions != null ? { instructions: subject.instructions } : {},
45
+ ...subject.toolNames != null ? { skillNames: [...subject.toolNames].sort() } : {},
46
+ ...subject.scope ? { scopeName: subject.scope.name, scopeTemplate: subject.scope.template } : {},
47
+ ...subject.bom ? { bom: subject.bom } : {}
48
+ };
49
+ }
50
+ function manifestBom(tools) {
51
+ return {
52
+ partial: true,
53
+ tools: [...tools].map((t) => ({
54
+ name: t.name,
55
+ description: t.description,
56
+ ...t.inputSchema !== void 0 ? { inputSchema: t.inputSchema } : {}
57
+ })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0),
58
+ actions: [],
59
+ skills: [],
60
+ subagents: []
61
+ };
62
+ }
63
+ function stableStringify(value) {
64
+ if (value === void 0) return "null";
65
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
66
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
67
+ const keys = Object.keys(value).sort();
68
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
69
+ }
70
+ var SCOPE_CASE_PLACEHOLDER = "{case}";
71
+ function isValidScopeTemplate(template) {
72
+ return template.includes(SCOPE_CASE_PLACEHOLDER);
73
+ }
74
+
75
+ // src/version.ts
76
+ import { toJsonSchema } from "@valibot/to-json-schema";
77
+ function toolManifestEntry(tool) {
78
+ let inputSchema;
79
+ if (tool.input != null) {
80
+ try {
81
+ inputSchema = toJsonSchema(tool.input, {
82
+ errorMode: "ignore"
83
+ });
84
+ } catch {
85
+ inputSchema = void 0;
86
+ }
87
+ }
88
+ return {
89
+ name: tool.name,
90
+ description: tool.description,
91
+ ...inputSchema !== void 0 ? { inputSchema } : {}
92
+ };
93
+ }
94
+ var isManifestEntry = (t) => "inputSchema" in t || !("input" in t);
95
+ function agentVersion(def) {
96
+ if (!def.agentName.trim()) throw new TypeError("agentName is empty");
97
+ if (typeof def.model !== "string" || !def.model.trim())
98
+ throw new TypeError("model is empty");
99
+ const manifest = def.tools.map(
100
+ (t) => isManifestEntry(t) ? t : toolManifestEntry(t)
101
+ );
102
+ const iHash = instructionsHash(def.instructions);
103
+ const tHash = toolManifestHash(manifest);
104
+ return {
105
+ agentName: def.agentName,
106
+ model: def.model,
107
+ instructionsHash: iHash,
108
+ toolsHash: tHash,
109
+ versionHash: subjectVersionHash({
110
+ agentName: def.agentName,
111
+ instructionsHash: iHash,
112
+ toolManifestHash: tHash,
113
+ model: def.model
114
+ }),
115
+ toolNames: manifest.map((t) => t.name).sort()
116
+ };
117
+ }
118
+ var agentVersionHash = (def) => agentVersion(def).versionHash;
119
+
120
+ export {
121
+ instructionsHash,
122
+ toolManifestHash,
123
+ subjectProblem,
124
+ subjectVersionHash,
125
+ subjectRegistration,
126
+ manifestBom,
127
+ isValidScopeTemplate,
128
+ toolManifestEntry,
129
+ agentVersion,
130
+ agentVersionHash
131
+ };
@@ -0,0 +1,195 @@
1
+ import { FlueInstrumentation } from '@flue/runtime';
2
+ import { A as AgentDefinition, a as AgentVersion } from './version-DU6_w67Z.js';
3
+ export { T as ToolLike, b as agentVersion, c as agentVersionHash, t as toolManifestEntry } from './version-DU6_w67Z.js';
4
+ export { ToolManifestEntry } from '@repo/schema/stamping';
5
+
6
+ declare const FEEDBACK_TYPES: readonly ["approval.approved", "approval.rejected", "approval.revised", "case.takeover", "case.handback", "case.hold", "edit.sent"];
7
+ type FeedbackType = (typeof FEEDBACK_TYPES)[number];
8
+ declare const REASON_CODES: readonly ["wrong_facts", "wrong_action", "tone", "policy", "duplicate", "other"];
9
+ type ReasonCode = (typeof REASON_CODES)[number];
10
+ type EditStats = {
11
+ charsBefore: number;
12
+ charsAfter: number;
13
+ editDistance: number;
14
+ /** editDistance / max(charsBefore, charsAfter): 0 = untouched, 1 = rewritten */
15
+ editRatio: number;
16
+ };
17
+ /** One human signal about an agent output. */
18
+ type FeedbackEvent = {
19
+ type: FeedbackType;
20
+ /** The conversation the output belongs to (Runbook: the case id). */
21
+ conversationId: string;
22
+ /** Idempotency key; a UUID is generated when absent. */
23
+ id?: string;
24
+ /** When the human acted; now when absent. */
25
+ occurredAt?: Date | string;
26
+ submissionId?: string;
27
+ toolCallId?: string;
28
+ outputId?: string;
29
+ /** The run's trace and span, when the app has them (hex32 / hex16). */
30
+ traceId?: string;
31
+ spanId?: string;
32
+ agentName?: string;
33
+ actionType?: string;
34
+ /** hashActor(userId, salt) — never a raw id or email. */
35
+ actorHash?: string;
36
+ /** Required for approval.rejected. */
37
+ reasonCode?: ReasonCode;
38
+ /** Whether the human wrote a note; the note itself is never sent. */
39
+ notePresent?: boolean;
40
+ /** edit.sent / approval.revised: how much the human changed (editStats). */
41
+ edit?: Partial<EditStats>;
42
+ decisionLatencyMs?: number;
43
+ /** Flat string/number/boolean map, at most 32 keys. */
44
+ attributes?: Record<string, string | number | boolean>;
45
+ };
46
+ /** The snake_case event POST /v1/feedback takes. */
47
+ type WireFeedbackEvent = {
48
+ id: string;
49
+ type: FeedbackType;
50
+ occurred_at: string;
51
+ conversation_id: string;
52
+ submission_id?: string;
53
+ tool_call_id?: string;
54
+ output_id?: string;
55
+ trace_id?: string;
56
+ span_id?: string;
57
+ agent_name?: string;
58
+ action_type?: string;
59
+ actor_hash?: string;
60
+ reason_code?: ReasonCode;
61
+ note_present?: boolean;
62
+ edit?: {
63
+ chars_before?: number;
64
+ chars_after?: number;
65
+ edit_distance?: number;
66
+ edit_ratio?: number;
67
+ };
68
+ decision_latency_ms?: number;
69
+ attributes?: Record<string, string | number | boolean>;
70
+ };
71
+ /**
72
+ * A salted sha256 of an actor id, as lowercase hex. Use one secret salt per
73
+ * deployment (not per event) so the same person hashes the same way and
74
+ * Eval Studio can count distinct reviewers without learning who they are.
75
+ */
76
+ declare function hashActor(id: string, salt: string): string;
77
+ /**
78
+ * How much a human changed an agent's draft, computed locally — the texts
79
+ * never leave the process, only these four numbers do.
80
+ */
81
+ declare function editStats(before: string, after: string): EditStats;
82
+ /**
83
+ * The wire form of an event, or a TypeError naming what is wrong. The final
84
+ * check is the server's own validator (@repo/schema/feedback-wire, bundled
85
+ * into the SDK), so an event that gets queued is an event POST /v1/feedback
86
+ * accepts: a bad one throws here, at the call site, instead of sinking the
87
+ * batch it would have been sent in.
88
+ */
89
+ declare function toWireEvent(e: FeedbackEvent): WireFeedbackEvent;
90
+
91
+ declare const EVAL_STUDIO_INSTRUMENTATION_KEY: unique symbol;
92
+ declare const ATTR_SUBJECT_VERSION_ID = "eval.subject_version_id";
93
+ declare const ATTR_VERSION_HASH = "eval.version_hash";
94
+ declare const ATTR_COST_USD = "flue.cost.usd";
95
+ /** What the stamp needs per agent, kept current by the client. */
96
+ type VersionLookup = (agentName: string) => {
97
+ versionHash: string;
98
+ subjectVersionId: string | null;
99
+ } | undefined;
100
+ type InstrumentationOptions = {
101
+ /**
102
+ * The @flue/opentelemetry instrumentation to compose with
103
+ * (createOpenTelemetryInstrumentation()). Install the result INSTEAD of it.
104
+ * Without it, install this after the OpenTelemetry one: stamping still
105
+ * works, the cost bridge cannot (the chat span has already ended).
106
+ */
107
+ otel?: FlueInstrumentation;
108
+ /** Set flue.cost.usd on chat spans that lack it. Default true. */
109
+ costBridge?: boolean;
110
+ };
111
+ declare function createInstrumentation(lookup: VersionLookup, options?: InstrumentationOptions): FlueInstrumentation;
112
+
113
+ type TransportOptions = {
114
+ fetch?: typeof fetch;
115
+ /** Attempts per request, first try included. Default 5. */
116
+ maxAttempts?: number;
117
+ /** First backoff delay; doubles per attempt, with jitter. Default 500 ms. */
118
+ baseDelayMs?: number;
119
+ /** Backoff ceiling. Default 30 s. */
120
+ maxDelayMs?: number;
121
+ /** Per-attempt timeout. Default 10 s. */
122
+ timeoutMs?: number;
123
+ /** Injectable for tests. */
124
+ sleep?: (ms: number) => Promise<void>;
125
+ random?: () => number;
126
+ };
127
+ type PostResult = {
128
+ ok: true;
129
+ status: number;
130
+ body: unknown;
131
+ } | {
132
+ ok: false;
133
+ status: number | null;
134
+ error: string;
135
+ retryable: boolean;
136
+ /** The parsed error body, when the server sent one (e.g. 400 `invalid`). */
137
+ body?: unknown;
138
+ };
139
+ declare function backoffDelay(attempt: number, opts: Pick<TransportOptions, "baseDelayMs" | "maxDelayMs" | "random">, retryAfterSeconds?: number): number;
140
+ /** POST JSON with the project key, retrying what is worth retrying. */
141
+ declare function postWithRetry(url: string, projectKey: string, body: unknown, opts?: TransportOptions): Promise<PostResult>;
142
+
143
+ type EvalStudioOptions = {
144
+ /** Eval Studio base URL. Default EVAL_STUDIO_URL; absent → inert. */
145
+ endpoint?: string;
146
+ /** Project API key with the `ingest` scope. Default EVAL_STUDIO_PROJECT_KEY (or EVAL_STUDIO_API_KEY). */
147
+ projectKey?: string;
148
+ /**
149
+ * The build this process runs (decision 5: the git SHA, recorded beside
150
+ * the version). Default OTEL_SERVICE_VERSION, else GIT_SHA. A hex SHA is
151
+ * also sent as the version's git_sha.
152
+ */
153
+ serviceVersion?: string;
154
+ /** Events per POST. Default 100 (server cap 500); a batch also stays under ~900 KB. */
155
+ batchSize?: number;
156
+ /** Auto-flush period for queued feedback. Default 2000 ms. */
157
+ flushIntervalMs?: number;
158
+ /** Queue bound; the oldest events are dropped past it. Default 10 000. */
159
+ maxQueueSize?: number;
160
+ /** Send each version's instruction text as provenance. Default true. */
161
+ sendInstructions?: boolean;
162
+ /** Called on every dropped event or failed request (never throws into the app). */
163
+ onError?: (error: EvalStudioError) => void;
164
+ } & TransportOptions;
165
+ type EvalStudioError = {
166
+ kind: "register" | "feedback" | "dropped";
167
+ message: string;
168
+ status?: number | null;
169
+ count?: number;
170
+ };
171
+ type RegisteredAgentVersion = AgentVersion & {
172
+ /** Eval Studio's id for the version; null when inert or unreachable. */
173
+ subjectVersionId: string | null;
174
+ };
175
+ type EvalStudio = {
176
+ /** Hash an agent version, stamp it from now on, and register it. */
177
+ registerAgentVersion: (def: AgentDefinition) => Promise<RegisteredAgentVersion>;
178
+ /** The Flue instrumentation: version stamps + the cost bridge. */
179
+ instrumentation: (options?: InstrumentationOptions) => FlueInstrumentation;
180
+ /** Queue one human feedback event (validated now, sent in a batch). */
181
+ feedback: (event: FeedbackEvent) => void;
182
+ /** Queue several. */
183
+ feedbackBatch: (events: FeedbackEvent[]) => void;
184
+ /** Send everything queued; resolves when the queue is empty or a send failed. */
185
+ flush: () => Promise<void>;
186
+ /** Stop the timer, flush, and refuse further events. */
187
+ shutdown: () => Promise<void>;
188
+ /** Versions registered in this process, by agent name. */
189
+ versions: () => ReadonlyMap<string, RegisteredAgentVersion>;
190
+ /** Whether this client sends anything. */
191
+ readonly enabled: boolean;
192
+ };
193
+ declare function createEvalStudio(options?: EvalStudioOptions): EvalStudio;
194
+
195
+ export { ATTR_COST_USD, ATTR_SUBJECT_VERSION_ID, ATTR_VERSION_HASH, AgentDefinition, AgentVersion, EVAL_STUDIO_INSTRUMENTATION_KEY, type EditStats, type EvalStudio, type EvalStudioError, type EvalStudioOptions, FEEDBACK_TYPES, type FeedbackEvent, type FeedbackType, type InstrumentationOptions, type PostResult, REASON_CODES, type ReasonCode, type RegisteredAgentVersion, type TransportOptions, type VersionLookup, type WireFeedbackEvent, backoffDelay, createEvalStudio, createInstrumentation, editStats, hashActor, postWithRetry, toWireEvent };