@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/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
|
-
#
|
|
1
|
+
# @eval-studio/flue
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|