@intentface/latch-langfuse 0.9.1
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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/client.d.ts +13 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +21 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/otel.d.ts +22 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +43 -0
- package/dist/otel.js.map +1 -0
- package/dist/reads.d.ts +70 -0
- package/dist/reads.d.ts.map +1 -0
- package/dist/reads.js +158 -0
- package/dist/reads.js.map +1 -0
- package/dist/reads.test.d.ts +2 -0
- package/dist/reads.test.d.ts.map +1 -0
- package/dist/reads.test.js +150 -0
- package/dist/reads.test.js.map +1 -0
- package/dist/scores.d.ts +49 -0
- package/dist/scores.d.ts.map +1 -0
- package/dist/scores.js +54 -0
- package/dist/scores.js.map +1 -0
- package/dist/telemetry.d.ts +55 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +161 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/trace-id.d.ts +18 -0
- package/dist/trace-id.d.ts.map +1 -0
- package/dist/trace-id.js +23 -0
- package/dist/trace-id.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Intentface
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @intentface/latch-langfuse
|
|
2
|
+
|
|
3
|
+
Langfuse observability for Latch. Implements `@intentface/latch-core`'s
|
|
4
|
+
dependency-free `telemetry` seam so any Latch host gets tracing, online evals,
|
|
5
|
+
and user feedback with one line — and exposes score/trace helpers platforms use
|
|
6
|
+
to read data back.
|
|
7
|
+
|
|
8
|
+
## Tracing
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createRuntime } from "@intentface/latch-core";
|
|
12
|
+
import { langfuseTelemetry, isLangfuseConfigured } from "@intentface/latch-langfuse";
|
|
13
|
+
|
|
14
|
+
const runtime = createRuntime({
|
|
15
|
+
// …storage, agents, etc…
|
|
16
|
+
telemetry: isLangfuseConfigured()
|
|
17
|
+
? langfuseTelemetry({ ownerTag: (p) => ownerKey(p) })
|
|
18
|
+
: undefined,
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Every agent run becomes one Langfuse trace whose id is derived deterministically
|
|
23
|
+
from the run id (`traceIdForRun`), tagged `owner:` / `agent:` / `run:`, with
|
|
24
|
+
`sessionId` = the chat id and `userId` = `ownerTag(principal)`. Reads and scores
|
|
25
|
+
address a trace from the run id alone — no lookup.
|
|
26
|
+
|
|
27
|
+
Configuration is read from the environment by the Langfuse SDK:
|
|
28
|
+
`LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_BASE_URL` (defaults to
|
|
29
|
+
`https://cloud.langfuse.com`). With no keys, `isLangfuseConfigured()` is `false`
|
|
30
|
+
and the seam should be left `undefined` — zero behavior change.
|
|
31
|
+
|
|
32
|
+
## Scores
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { postScore } from "@intentface/latch-langfuse";
|
|
36
|
+
|
|
37
|
+
// judge verdict or user feedback on a run's trace
|
|
38
|
+
await postScore({ runId, name: "helpfulness", value: 1, comment, dataType: "BOOLEAN" });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Reading traces (server-side, owner-scoped)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { listOwnerTraces } from "@intentface/latch-langfuse";
|
|
45
|
+
|
|
46
|
+
// the tenant's recent traces + their scores, newest first
|
|
47
|
+
const traces = await listOwnerTraces({ ownerTag, agent, limit: 50 });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`ownerTag` is the tenancy boundary — the platform proxies all reads and filters
|
|
51
|
+
on it, so end users never receive Langfuse credentials.
|
|
52
|
+
|
|
53
|
+
## Public API
|
|
54
|
+
|
|
55
|
+
| Export | Purpose |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `langfuseTelemetry(opts)` | The `LatchTelemetry` implementation for `RuntimeConfig.telemetry`. |
|
|
58
|
+
| `traceIdForRun(runId)` | Deterministic Langfuse trace id for a run. |
|
|
59
|
+
| `postScore(input)` | Attach a score (judge/feedback) to a run's trace. |
|
|
60
|
+
| `listOwnerTraces(args)` | Owner-scoped trace feed with scores joined in. |
|
|
61
|
+
| `isLangfuseConfigured()` | Whether the env credentials are present. |
|
|
62
|
+
| `getLangfuseClient()` | The shared `LangfuseClient`. |
|
|
63
|
+
| `ensureTracing()` / `flushLangfuse()` | OTEL provider setup + serverless flush. |
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LangfuseClient } from "@langfuse/client";
|
|
2
|
+
/**
|
|
3
|
+
* True when Langfuse credentials are present (`LANGFUSE_PUBLIC_KEY` +
|
|
4
|
+
* `LANGFUSE_SECRET_KEY`; `LANGFUSE_BASE_URL` optional, defaults to
|
|
5
|
+
* cloud.langfuse.com). The telemetry seam should be left undefined otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isLangfuseConfigured(): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Process-wide `LangfuseClient` (reads credentials from env). Cached on
|
|
10
|
+
* `globalThis` so Vite SSR module reloads don't multiply clients.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getLangfuseClient(): LangfuseClient;
|
|
13
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAI9C;AAID;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,CAIlD"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LangfuseClient } from "@langfuse/client";
|
|
2
|
+
/**
|
|
3
|
+
* True when Langfuse credentials are present (`LANGFUSE_PUBLIC_KEY` +
|
|
4
|
+
* `LANGFUSE_SECRET_KEY`; `LANGFUSE_BASE_URL` optional, defaults to
|
|
5
|
+
* cloud.langfuse.com). The telemetry seam should be left undefined otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export function isLangfuseConfigured() {
|
|
8
|
+
return Boolean(process.env.LANGFUSE_PUBLIC_KEY && process.env.LANGFUSE_SECRET_KEY);
|
|
9
|
+
}
|
|
10
|
+
const CLIENT_KEY = "__latchLangfuseClient";
|
|
11
|
+
/**
|
|
12
|
+
* Process-wide `LangfuseClient` (reads credentials from env). Cached on
|
|
13
|
+
* `globalThis` so Vite SSR module reloads don't multiply clients.
|
|
14
|
+
*/
|
|
15
|
+
export function getLangfuseClient() {
|
|
16
|
+
const g = globalThis;
|
|
17
|
+
if (!g[CLIENT_KEY])
|
|
18
|
+
g[CLIENT_KEY] = new LangfuseClient();
|
|
19
|
+
return g[CLIENT_KEY];
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,CACZ,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CACnE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,GAAG,uBAAuB,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,CAAC,GAAG,UAAqC,CAAC;IAChD,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC;QAAE,CAAC,CAAC,UAAU,CAAC,GAAG,IAAI,cAAc,EAAE,CAAC;IACzD,OAAO,CAAC,CAAC,UAAU,CAAmB,CAAC;AACzC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { isLangfuseConfigured, getLangfuseClient } from "./client.js";
|
|
2
|
+
export { ensureTracing, flushLangfuse } from "./otel.js";
|
|
3
|
+
export { traceIdForRun, scoreIdForRun } from "./trace-id.js";
|
|
4
|
+
export { langfuseTelemetry, type LangfuseTelemetryOptions, } from "./telemetry.js";
|
|
5
|
+
export { postScore, type PostScoreInput } from "./scores.js";
|
|
6
|
+
export { listOwnerTraces, listAdministeredTraces, AGENT_OWNER_TAG, type TraceSummary, type TraceScore, } from "./reads.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EACL,iBAAiB,EACjB,KAAK,wBAAwB,GAC9B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,eAAe,EACf,KAAK,YAAY,EACjB,KAAK,UAAU,GAChB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { isLangfuseConfigured, getLangfuseClient } from "./client.js";
|
|
2
|
+
export { ensureTracing, flushLangfuse } from "./otel.js";
|
|
3
|
+
export { traceIdForRun, scoreIdForRun } from "./trace-id.js";
|
|
4
|
+
export { langfuseTelemetry, } from "./telemetry.js";
|
|
5
|
+
export { postScore } from "./scores.js";
|
|
6
|
+
export { listOwnerTraces, listAdministeredTraces, AGENT_OWNER_TAG, } from "./reads.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EACL,iBAAiB,GAElB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,eAAe,GAGhB,MAAM,YAAY,CAAC"}
|
package/dist/otel.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LangfuseSpanProcessor } from "@langfuse/otel";
|
|
2
|
+
/**
|
|
3
|
+
* Register the global OTEL tracer provider with the Langfuse span processor,
|
|
4
|
+
* once per process. Guarded on `globalThis` so Vite/Nitro SSR module reloads
|
|
5
|
+
* (dev HMR) don't register a second provider. Credentials come from env
|
|
6
|
+
* (`LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` / `LANGFUSE_BASE_URL`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function ensureTracing(opts?: {
|
|
9
|
+
/** Trace environment shown in Langfuse (e.g. "production" | "development"). */
|
|
10
|
+
environment?: string;
|
|
11
|
+
}): LangfuseSpanProcessor;
|
|
12
|
+
/**
|
|
13
|
+
* The environment tracing was set up with (via `ensureTracing`), so out-of-band
|
|
14
|
+
* score writes can be recorded in the SAME Langfuse environment as their trace
|
|
15
|
+
* instead of a mismatched one. Undefined until `ensureTracing` has run in this
|
|
16
|
+
* process (e.g. a cold invocation that only posts a score) — callers fall back
|
|
17
|
+
* to the env var.
|
|
18
|
+
*/
|
|
19
|
+
export declare function tracingEnvironment(): string | undefined;
|
|
20
|
+
/** Flush pending spans to Langfuse (serverless-safe; no-op if never set up). */
|
|
21
|
+
export declare function flushLangfuse(): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=otel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel.d.ts","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAYvD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE;IACnC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,qBAAqB,CAexB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAGvD;AAED,gFAAgF;AAChF,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAInD"}
|
package/dist/otel.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { LangfuseSpanProcessor } from "@langfuse/otel";
|
|
2
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
3
|
+
const OTEL_KEY = "__latchLangfuseOtel";
|
|
4
|
+
/**
|
|
5
|
+
* Register the global OTEL tracer provider with the Langfuse span processor,
|
|
6
|
+
* once per process. Guarded on `globalThis` so Vite/Nitro SSR module reloads
|
|
7
|
+
* (dev HMR) don't register a second provider. Credentials come from env
|
|
8
|
+
* (`LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` / `LANGFUSE_BASE_URL`).
|
|
9
|
+
*/
|
|
10
|
+
export function ensureTracing(opts) {
|
|
11
|
+
const g = globalThis;
|
|
12
|
+
if (!g[OTEL_KEY]) {
|
|
13
|
+
const processor = new LangfuseSpanProcessor({
|
|
14
|
+
environment: opts?.environment,
|
|
15
|
+
});
|
|
16
|
+
const provider = new NodeTracerProvider({ spanProcessors: [processor] });
|
|
17
|
+
provider.register();
|
|
18
|
+
g[OTEL_KEY] = {
|
|
19
|
+
provider,
|
|
20
|
+
processor,
|
|
21
|
+
environment: opts?.environment,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return g[OTEL_KEY].processor;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The environment tracing was set up with (via `ensureTracing`), so out-of-band
|
|
28
|
+
* score writes can be recorded in the SAME Langfuse environment as their trace
|
|
29
|
+
* instead of a mismatched one. Undefined until `ensureTracing` has run in this
|
|
30
|
+
* process (e.g. a cold invocation that only posts a score) — callers fall back
|
|
31
|
+
* to the env var.
|
|
32
|
+
*/
|
|
33
|
+
export function tracingEnvironment() {
|
|
34
|
+
const g = globalThis;
|
|
35
|
+
return g[OTEL_KEY]?.environment;
|
|
36
|
+
}
|
|
37
|
+
/** Flush pending spans to Langfuse (serverless-safe; no-op if never set up). */
|
|
38
|
+
export async function flushLangfuse() {
|
|
39
|
+
const g = globalThis;
|
|
40
|
+
const otel = g[OTEL_KEY];
|
|
41
|
+
await otel?.processor.forceFlush();
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=otel.js.map
|
package/dist/otel.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel.js","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAEnE,MAAM,QAAQ,GAAG,qBAAqB,CAAC;AASvC;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAG7B;IACC,MAAM,CAAC,GAAG,UAAqC,CAAC;IAChD,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjB,MAAM,SAAS,GAAG,IAAI,qBAAqB,CAAC;YAC1C,WAAW,EAAE,IAAI,EAAE,WAAW;SAC/B,CAAC,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,kBAAkB,CAAC,EAAE,cAAc,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QACzE,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACpB,CAAC,CAAC,QAAQ,CAAC,GAAG;YACZ,QAAQ;YACR,SAAS;YACT,WAAW,EAAE,IAAI,EAAE,WAAW;SACP,CAAC;IAC5B,CAAC;IACD,OAAQ,CAAC,CAAC,QAAQ,CAAmB,CAAC,SAAS,CAAC;AAClD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,CAAC,GAAG,UAAqC,CAAC;IAChD,OAAQ,CAAC,CAAC,QAAQ,CAA+B,EAAE,WAAW,CAAC;AACjE,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,CAAC,GAAG,UAAqC,CAAC;IAChD,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,CAA8B,CAAC;IACtD,MAAM,IAAI,EAAE,SAAS,CAAC,UAAU,EAAE,CAAC;AACrC,CAAC"}
|
package/dist/reads.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface TraceScore {
|
|
2
|
+
name: string;
|
|
3
|
+
value: number | string | null;
|
|
4
|
+
comment?: string;
|
|
5
|
+
dataType?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TraceSummary {
|
|
8
|
+
/** Langfuse trace id. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** The latch run id (parsed from the `run:` tag), if present. */
|
|
11
|
+
runId?: string;
|
|
12
|
+
/** Agent name (the trace name / `agent:` tag). */
|
|
13
|
+
agent?: string;
|
|
14
|
+
/** The trace's Langfuse `userId` — the owner tag of whoever RAN the turn. */
|
|
15
|
+
userId?: string;
|
|
16
|
+
/** The `agent-owner:` tag, when the run's agent had an owner key. */
|
|
17
|
+
agentOwner?: string;
|
|
18
|
+
timestamp: string;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
input?: unknown;
|
|
21
|
+
output?: unknown;
|
|
22
|
+
/** Latency in seconds. */
|
|
23
|
+
latency?: number;
|
|
24
|
+
/** Total cost in USD. */
|
|
25
|
+
totalCost?: number;
|
|
26
|
+
/** Deep link into the Langfuse UI. */
|
|
27
|
+
htmlPath?: string;
|
|
28
|
+
environment?: string;
|
|
29
|
+
scores: TraceScore[];
|
|
30
|
+
}
|
|
31
|
+
/** The `agent-owner:` tag prefix — written by telemetry, filtered on here. */
|
|
32
|
+
export declare const AGENT_OWNER_TAG = "agent-owner:";
|
|
33
|
+
/**
|
|
34
|
+
* Recent traces for one owner (tenant), newest first, with their scores
|
|
35
|
+
* joined in. Reads are meant to run server-side in the platform — the owner
|
|
36
|
+
* filter is the tenancy boundary, so never expose this to callers who don't
|
|
37
|
+
* own `ownerTag`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function listOwnerTraces(args: {
|
|
40
|
+
ownerTag: string;
|
|
41
|
+
/** Optional agent-name filter. */
|
|
42
|
+
agent?: string;
|
|
43
|
+
limit?: number;
|
|
44
|
+
page?: number;
|
|
45
|
+
}): Promise<TraceSummary[]>;
|
|
46
|
+
/**
|
|
47
|
+
* The observability feed for a reader who administers agents: every recent run of
|
|
48
|
+
* the agents owned by `agentOwnerTags` — INCLUDING runs by other users — unioned
|
|
49
|
+
* with the reader's own runs (`ownerTag`).
|
|
50
|
+
*
|
|
51
|
+
* Two sources, because they answer different questions. The `agent-owner:` tag
|
|
52
|
+
* finds other people's runs on my agents (the point of this feed). `ownerTag`
|
|
53
|
+
* keeps my own runs visible on agents I don't administer — code-registry agents
|
|
54
|
+
* carry no owner tag at all — and covers every trace written before the tag
|
|
55
|
+
* existed. The API's `tags` filter is conjunctive ("all of these tags"), so each
|
|
56
|
+
* owner key needs its own query; results are merged and de-duplicated by trace id.
|
|
57
|
+
*
|
|
58
|
+
* Tenancy: `agentOwnerTags` and `ownerTag` together ARE the boundary. Pass only
|
|
59
|
+
* keys the caller provably holds (see `evalOwnerKeysOf`) — never a client value.
|
|
60
|
+
*/
|
|
61
|
+
export declare function listAdministeredTraces(args: {
|
|
62
|
+
/** Owner keys of the agents the reader administers (personal + org). */
|
|
63
|
+
agentOwnerTags: string[];
|
|
64
|
+
/** The reader's own owner tag, so its own runs stay visible. */
|
|
65
|
+
ownerTag: string;
|
|
66
|
+
/** Optional agent-name filter. */
|
|
67
|
+
agent?: string;
|
|
68
|
+
limit?: number;
|
|
69
|
+
}): Promise<TraceSummary[]>;
|
|
70
|
+
//# sourceMappingURL=reads.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reads.d.ts","sourceRoot":"","sources":["../src/reads.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,yBAAyB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAKD,8EAA8E;AAC9E,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAgF9C;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAY1B;AAeD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,wEAAwE;IACxE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA2C1B"}
|
package/dist/reads.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { getLangfuseClient } from "./client.js";
|
|
2
|
+
const tagValue = (tags, prefix) => tags.find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
3
|
+
/** The `agent-owner:` tag prefix — written by telemetry, filtered on here. */
|
|
4
|
+
export const AGENT_OWNER_TAG = "agent-owner:";
|
|
5
|
+
/**
|
|
6
|
+
* Scores for exactly these traces via the Scores v3 API (`scores.getMany`/v2 is
|
|
7
|
+
* deprecated and 404s on Langfuse v4+). The v3 endpoint takes a comma-separated
|
|
8
|
+
* `traceId` filter and returns the subject trace under `subject` when requested
|
|
9
|
+
* via `fields`, so scores reliably attach to the traces we asked about instead of
|
|
10
|
+
* a fixed 100-row owner-wide slice.
|
|
11
|
+
*/
|
|
12
|
+
async function scoresFor(traceIds) {
|
|
13
|
+
const scoresByTrace = new Map();
|
|
14
|
+
if (traceIds.length === 0)
|
|
15
|
+
return scoresByTrace;
|
|
16
|
+
const client = getLangfuseClient();
|
|
17
|
+
const traceIdFilter = traceIds.join(",");
|
|
18
|
+
// `getManyV3` is cursor-paginated (max 100/page). A single page of traces can
|
|
19
|
+
// carry >100 scores (rubrics + feedback per run), so follow the cursor until
|
|
20
|
+
// it's absent — otherwise later scores silently drop off the feed.
|
|
21
|
+
let cursor;
|
|
22
|
+
do {
|
|
23
|
+
const scores = await client.api.scoresV3.getManyV3({
|
|
24
|
+
traceId: traceIdFilter,
|
|
25
|
+
fields: "details,subject",
|
|
26
|
+
limit: 100,
|
|
27
|
+
...(cursor ? { cursor } : {}),
|
|
28
|
+
});
|
|
29
|
+
for (const s of scores.data ?? []) {
|
|
30
|
+
const subject = s.subject;
|
|
31
|
+
if (subject?.kind !== "trace" || !subject.id)
|
|
32
|
+
continue;
|
|
33
|
+
const list = scoresByTrace.get(subject.id) ?? [];
|
|
34
|
+
// v3 returns BOOLEAN scores as a JS boolean (v2 returned 1/0). Normalize
|
|
35
|
+
// to a number so binary and numeric scores share one numeric contract
|
|
36
|
+
// (aggregates/averaging); categorical scores stay strings.
|
|
37
|
+
const raw = s.value;
|
|
38
|
+
const value = typeof raw === "boolean" ? (raw ? 1 : 0) : (raw ?? null);
|
|
39
|
+
list.push({
|
|
40
|
+
name: s.name,
|
|
41
|
+
value,
|
|
42
|
+
comment: s.comment ?? undefined,
|
|
43
|
+
dataType: s.dataType,
|
|
44
|
+
});
|
|
45
|
+
scoresByTrace.set(subject.id, list);
|
|
46
|
+
}
|
|
47
|
+
cursor = scores.meta?.cursor;
|
|
48
|
+
} while (cursor);
|
|
49
|
+
return scoresByTrace;
|
|
50
|
+
}
|
|
51
|
+
const toSummary = (t, scores) => ({
|
|
52
|
+
id: t.id,
|
|
53
|
+
runId: tagValue(t.tags ?? [], "run:"),
|
|
54
|
+
agent: t.name ?? tagValue(t.tags ?? [], "agent:"),
|
|
55
|
+
userId: t.userId ?? undefined,
|
|
56
|
+
agentOwner: tagValue(t.tags ?? [], AGENT_OWNER_TAG),
|
|
57
|
+
timestamp: t.timestamp,
|
|
58
|
+
sessionId: t.sessionId ?? undefined,
|
|
59
|
+
input: t.input,
|
|
60
|
+
output: t.output,
|
|
61
|
+
latency: t.latency ?? undefined,
|
|
62
|
+
totalCost: t.totalCost ?? undefined,
|
|
63
|
+
htmlPath: t.htmlPath,
|
|
64
|
+
environment: t.environment,
|
|
65
|
+
scores,
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Recent traces for one owner (tenant), newest first, with their scores
|
|
69
|
+
* joined in. Reads are meant to run server-side in the platform — the owner
|
|
70
|
+
* filter is the tenancy boundary, so never expose this to callers who don't
|
|
71
|
+
* own `ownerTag`.
|
|
72
|
+
*/
|
|
73
|
+
export async function listOwnerTraces(args) {
|
|
74
|
+
const client = getLangfuseClient();
|
|
75
|
+
const traces = await client.api.trace.list({
|
|
76
|
+
userId: args.ownerTag,
|
|
77
|
+
...(args.agent ? { name: args.agent } : {}),
|
|
78
|
+
limit: args.limit ?? 50,
|
|
79
|
+
page: args.page,
|
|
80
|
+
orderBy: "timestamp.desc",
|
|
81
|
+
});
|
|
82
|
+
const rows = (traces.data ?? []);
|
|
83
|
+
const scores = await scoresFor(rows.map((t) => t.id));
|
|
84
|
+
return rows.map((t) => toSummary(t, scores.get(t.id) ?? []));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Newest-first by trace timestamp; unparseable timestamps sort last. Never
|
|
88
|
+
* returns NaN — a NaN comparator leaves Array#sort's ordering unspecified.
|
|
89
|
+
*/
|
|
90
|
+
const byNewest = (a, b) => {
|
|
91
|
+
const at = Date.parse(a.timestamp);
|
|
92
|
+
const bt = Date.parse(b.timestamp);
|
|
93
|
+
if (Number.isNaN(at) && Number.isNaN(bt))
|
|
94
|
+
return 0;
|
|
95
|
+
if (Number.isNaN(at))
|
|
96
|
+
return 1;
|
|
97
|
+
if (Number.isNaN(bt))
|
|
98
|
+
return -1;
|
|
99
|
+
return bt - at;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The observability feed for a reader who administers agents: every recent run of
|
|
103
|
+
* the agents owned by `agentOwnerTags` — INCLUDING runs by other users — unioned
|
|
104
|
+
* with the reader's own runs (`ownerTag`).
|
|
105
|
+
*
|
|
106
|
+
* Two sources, because they answer different questions. The `agent-owner:` tag
|
|
107
|
+
* finds other people's runs on my agents (the point of this feed). `ownerTag`
|
|
108
|
+
* keeps my own runs visible on agents I don't administer — code-registry agents
|
|
109
|
+
* carry no owner tag at all — and covers every trace written before the tag
|
|
110
|
+
* existed. The API's `tags` filter is conjunctive ("all of these tags"), so each
|
|
111
|
+
* owner key needs its own query; results are merged and de-duplicated by trace id.
|
|
112
|
+
*
|
|
113
|
+
* Tenancy: `agentOwnerTags` and `ownerTag` together ARE the boundary. Pass only
|
|
114
|
+
* keys the caller provably holds (see `evalOwnerKeysOf`) — never a client value.
|
|
115
|
+
*/
|
|
116
|
+
export async function listAdministeredTraces(args) {
|
|
117
|
+
const client = getLangfuseClient();
|
|
118
|
+
const limit = args.limit ?? 50;
|
|
119
|
+
const common = {
|
|
120
|
+
...(args.agent ? { name: args.agent } : {}),
|
|
121
|
+
limit,
|
|
122
|
+
orderBy: "timestamp.desc",
|
|
123
|
+
};
|
|
124
|
+
// De-duplicate the key list first: for a principal without a userId the
|
|
125
|
+
// personal and org keys collapse to the same string, which would double the
|
|
126
|
+
// API calls for identical rows.
|
|
127
|
+
const ownerTags = [...new Set(args.agentOwnerTags.filter((t) => t.length > 0))];
|
|
128
|
+
// Sources are independent and best-effort: one failing query (a transient 5xx,
|
|
129
|
+
// or a deployment whose API rejects the tag filter) degrades the feed to the
|
|
130
|
+
// other sources rather than emptying the page.
|
|
131
|
+
const pages = await Promise.allSettled([
|
|
132
|
+
...ownerTags.map((tag) => client.api.trace.list({ ...common, tags: [`${AGENT_OWNER_TAG}${tag}`] })),
|
|
133
|
+
client.api.trace.list({ ...common, userId: args.ownerTag }),
|
|
134
|
+
]);
|
|
135
|
+
const merged = new Map();
|
|
136
|
+
for (const page of pages) {
|
|
137
|
+
if (page.status === "rejected") {
|
|
138
|
+
console.warn("trace list source failed:", page.reason);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
for (const t of (page.value.data ?? []))
|
|
142
|
+
merged.set(t.id, t);
|
|
143
|
+
}
|
|
144
|
+
// Partial failure degrades the feed; TOTAL failure must throw. An empty array
|
|
145
|
+
// is indistinguishable from "this tenant has no traces", so swallowing it would
|
|
146
|
+
// replace the caller's last good page with a confident "No traces yet".
|
|
147
|
+
const failure = pages.find((p) => p.status === "rejected");
|
|
148
|
+
if (failure?.status === "rejected" && pages.every((p) => p.status === "rejected")) {
|
|
149
|
+
throw failure.reason;
|
|
150
|
+
}
|
|
151
|
+
// Each source returned up to `limit` rows; the union can exceed it, so re-sort
|
|
152
|
+
// across sources and take the newest `limit` — otherwise the cap would depend
|
|
153
|
+
// on which query a row came from.
|
|
154
|
+
const rows = [...merged.values()].sort(byNewest).slice(0, limit);
|
|
155
|
+
const scores = await scoresFor(rows.map((t) => t.id));
|
|
156
|
+
return rows.map((t) => toSummary(t, scores.get(t.id) ?? []));
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=reads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reads.js","sourceRoot":"","sources":["../src/reads.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAkChD,MAAM,QAAQ,GAAG,CAAC,IAAc,EAAE,MAAc,EAAsB,EAAE,CACtE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAE/D,8EAA8E;AAC9E,MAAM,CAAC,MAAM,eAAe,GAAG,cAAc,CAAC;AAkB9C;;;;;;GAMG;AACH,KAAK,UAAU,SAAS,CAAC,QAAkB;IACzC,MAAM,aAAa,GAAG,IAAI,GAAG,EAAwB,CAAC;IACtD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC;IAChD,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;IACnC,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzC,8EAA8E;IAC9E,6EAA6E;IAC7E,mEAAmE;IACnE,IAAI,MAA0B,CAAC;IAC/B,GAAG,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC;YACjD,OAAO,EAAE,aAAa;YACtB,MAAM,EAAE,iBAAiB;YACzB,KAAK,EAAE,GAAG;YACV,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9B,CAAC,CAAC;QACH,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;YAClC,MAAM,OAAO,GAAI,CAAkD,CAAC,OAAO,CAAC;YAC5E,IAAI,OAAO,EAAE,IAAI,KAAK,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE;gBAAE,SAAS;YACvD,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;YACjD,yEAAyE;YACzE,sEAAsE;YACtE,2DAA2D;YAC3D,MAAM,GAAG,GAAI,CAAkD,CAAC,KAAK,CAAC;YACtE,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;YACvE,IAAI,CAAC,IAAI,CAAC;gBACR,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,KAAK;gBACL,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,SAAS;gBAC/B,QAAQ,EAAG,CAA2B,CAAC,QAAQ;aAChD,CAAC,CAAC;YACH,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACtC,CAAC;QACD,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC;IAC/B,CAAC,QAAQ,MAAM,EAAE;IACjB,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,MAAM,SAAS,GAAG,CAAC,CAAW,EAAE,MAAoB,EAAgB,EAAE,CAAC,CAAC;IACtE,EAAE,EAAE,CAAC,CAAC,EAAE;IACR,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,MAAM,CAAC;IACrC,KAAK,EAAE,CAAC,CAAC,IAAI,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,QAAQ,CAAC;IACjD,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,SAAS;IAC7B,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,eAAe,CAAC;IACnD,SAAS,EAAE,CAAC,CAAC,SAAS;IACtB,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,SAAS;IACnC,KAAK,EAAE,CAAC,CAAC,KAAK;IACd,MAAM,EAAE,CAAC,CAAC,MAAM;IAChB,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,SAAS;IAC/B,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,SAAS;IACnC,QAAQ,EAAE,CAAC,CAAC,QAAQ;IACpB,WAAW,EAAE,CAAC,CAAC,WAAW;IAC1B,MAAM;CACP,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAMrC;IACC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;IACnC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;QACzC,MAAM,EAAE,IAAI,CAAC,QAAQ;QACrB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE;QACvB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,OAAO,EAAE,gBAAgB;KAC1B,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAe,CAAC;IAC/C,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED;;;GAGG;AACH,MAAM,QAAQ,GAAG,CAAC,CAAW,EAAE,CAAW,EAAU,EAAE;IACpD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACnC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAAE,OAAO,CAAC,CAAC;IACnD,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC;IAChC,OAAO,EAAE,GAAG,EAAE,CAAC;AACjB,CAAC,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,IAQ5C;IACC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;IAC/B,MAAM,MAAM,GAAG;QACb,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,KAAK;QACL,OAAO,EAAE,gBAAgB;KAC1B,CAAC;IACF,wEAAwE;IACxE,4EAA4E;IAC5E,gCAAgC;IAChC,MAAM,SAAS,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAChF,+EAA+E;IAC/E,6EAA6E;IAC7E,+CAA+C;IAC/C,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;QACrC,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACvB,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,eAAe,GAAG,GAAG,EAAE,CAAC,EAAE,CAAC,CACzE;QACD,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;KAC5D,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAC/B,OAAO,CAAC,IAAI,CAAC,2BAA2B,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YACvD,SAAS;QACX,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAe;YAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC7E,CAAC;IACD,8EAA8E;IAC9E,gFAAgF;IAChF,wEAAwE;IACxE,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC;IAC3D,IAAI,OAAO,EAAE,MAAM,KAAK,UAAU,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,CAAC,EAAE,CAAC;QAClF,MAAM,OAAO,CAAC,MAAM,CAAC;IACvB,CAAC;IACD,+EAA+E;IAC/E,8EAA8E;IAC9E,kCAAkC;IAClC,MAAM,IAAI,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACjE,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AAC/D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reads.test.d.ts","sourceRoot":"","sources":["../src/reads.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
/**
|
|
3
|
+
* The trace-list calls the reader made, in order, so a test can assert WHICH
|
|
4
|
+
* queries were issued (one per agent-owner tag, plus the caller's own userId).
|
|
5
|
+
*/
|
|
6
|
+
const calls = [];
|
|
7
|
+
let tracePages;
|
|
8
|
+
let scoreRows;
|
|
9
|
+
/** When set, the trace-list call matching this filter key rejects ("*" = all). */
|
|
10
|
+
let failFor;
|
|
11
|
+
vi.mock("./client.js", () => ({
|
|
12
|
+
getLangfuseClient: () => ({
|
|
13
|
+
api: {
|
|
14
|
+
trace: {
|
|
15
|
+
list: async (params) => {
|
|
16
|
+
calls.push(params);
|
|
17
|
+
// Key the canned page by the filter this call used.
|
|
18
|
+
const tags = params.tags;
|
|
19
|
+
const key = tags?.[0] ?? `user:${String(params.userId)}`;
|
|
20
|
+
if (failFor && (failFor === "*" || key === failFor)) {
|
|
21
|
+
throw new Error("langfuse unavailable");
|
|
22
|
+
}
|
|
23
|
+
return { data: tracePages[key] ?? [] };
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
scoresV3: {
|
|
27
|
+
getManyV3: async () => ({ data: scoreRows, meta: {} }),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
const { AGENT_OWNER_TAG, listAdministeredTraces } = await import("./reads.js");
|
|
33
|
+
const trace = (id, at, over = {}) => ({
|
|
34
|
+
id,
|
|
35
|
+
name: "my-agent",
|
|
36
|
+
timestamp: at,
|
|
37
|
+
tags: [`run:run-${id}`, `agent:my-agent`],
|
|
38
|
+
...over,
|
|
39
|
+
});
|
|
40
|
+
describe("listAdministeredTraces", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
calls.length = 0;
|
|
43
|
+
tracePages = {};
|
|
44
|
+
scoreRows = [];
|
|
45
|
+
failFor = undefined;
|
|
46
|
+
});
|
|
47
|
+
it("queries once per agent-owner key, plus the reader's own runs", async () => {
|
|
48
|
+
await listAdministeredTraces({
|
|
49
|
+
agentOwnerTags: ["org/me", "org"],
|
|
50
|
+
ownerTag: "org/me",
|
|
51
|
+
limit: 10,
|
|
52
|
+
});
|
|
53
|
+
expect(calls).toHaveLength(3);
|
|
54
|
+
expect(calls[0]).toMatchObject({ tags: [`${AGENT_OWNER_TAG}org/me`] });
|
|
55
|
+
expect(calls[1]).toMatchObject({ tags: [`${AGENT_OWNER_TAG}org`] });
|
|
56
|
+
expect(calls[2]).toMatchObject({ userId: "org/me" });
|
|
57
|
+
});
|
|
58
|
+
it("collapses duplicate owner keys into one query", async () => {
|
|
59
|
+
// An org-only principal's personal and org keys are the same string.
|
|
60
|
+
await listAdministeredTraces({
|
|
61
|
+
agentOwnerTags: ["org", "org", ""],
|
|
62
|
+
ownerTag: "org",
|
|
63
|
+
});
|
|
64
|
+
expect(calls).toHaveLength(2); // one tag query + the userId query
|
|
65
|
+
});
|
|
66
|
+
it("surfaces another user's run on an agent I administer", async () => {
|
|
67
|
+
tracePages[`${AGENT_OWNER_TAG}org`] = [
|
|
68
|
+
trace("t1", "2026-07-31T10:00:00Z", {
|
|
69
|
+
userId: "org/colleague",
|
|
70
|
+
tags: ["run:run-t1", "agent:my-agent", `${AGENT_OWNER_TAG}org`],
|
|
71
|
+
}),
|
|
72
|
+
];
|
|
73
|
+
const rows = await listAdministeredTraces({
|
|
74
|
+
agentOwnerTags: ["org"],
|
|
75
|
+
ownerTag: "org/me",
|
|
76
|
+
});
|
|
77
|
+
expect(rows).toHaveLength(1);
|
|
78
|
+
expect(rows[0]).toMatchObject({
|
|
79
|
+
runId: "run-t1",
|
|
80
|
+
userId: "org/colleague", // attribution: who ran it
|
|
81
|
+
agentOwner: "org", // whose agent it was
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
it("de-duplicates a trace returned by more than one source", async () => {
|
|
85
|
+
const t = trace("t1", "2026-07-31T10:00:00Z", { userId: "org/me" });
|
|
86
|
+
tracePages[`${AGENT_OWNER_TAG}org`] = [t];
|
|
87
|
+
tracePages["user:org/me"] = [t];
|
|
88
|
+
const rows = await listAdministeredTraces({
|
|
89
|
+
agentOwnerTags: ["org"],
|
|
90
|
+
ownerTag: "org/me",
|
|
91
|
+
});
|
|
92
|
+
expect(rows.map((r) => r.id)).toEqual(["t1"]);
|
|
93
|
+
});
|
|
94
|
+
it("re-sorts across sources and caps at the limit", async () => {
|
|
95
|
+
// Each source is newest-first on its own; the union must be too, or the cap
|
|
96
|
+
// would keep whichever query a row happened to come from.
|
|
97
|
+
tracePages[`${AGENT_OWNER_TAG}org`] = [
|
|
98
|
+
trace("old", "2026-07-01T00:00:00Z"),
|
|
99
|
+
trace("oldest", "2026-06-01T00:00:00Z"),
|
|
100
|
+
];
|
|
101
|
+
tracePages["user:org/me"] = [trace("newest", "2026-07-31T00:00:00Z")];
|
|
102
|
+
const rows = await listAdministeredTraces({
|
|
103
|
+
agentOwnerTags: ["org"],
|
|
104
|
+
ownerTag: "org/me",
|
|
105
|
+
limit: 2,
|
|
106
|
+
});
|
|
107
|
+
expect(rows.map((r) => r.id)).toEqual(["newest", "old"]);
|
|
108
|
+
});
|
|
109
|
+
it("joins scores in and normalizes booleans to numbers", async () => {
|
|
110
|
+
tracePages[`${AGENT_OWNER_TAG}org`] = [trace("t1", "2026-07-31T10:00:00Z")];
|
|
111
|
+
scoreRows = [
|
|
112
|
+
{ name: "greets", value: true, subject: { kind: "trace", id: "t1" }, dataType: "BOOLEAN" },
|
|
113
|
+
{ name: "user-feedback", value: 1, subject: { kind: "trace", id: "t1" } },
|
|
114
|
+
{ name: "ignored", value: 1, subject: { kind: "observation", id: "t1" } },
|
|
115
|
+
];
|
|
116
|
+
const rows = await listAdministeredTraces({
|
|
117
|
+
agentOwnerTags: ["org"],
|
|
118
|
+
ownerTag: "org/me",
|
|
119
|
+
});
|
|
120
|
+
expect(rows[0].scores).toEqual([
|
|
121
|
+
{ name: "greets", value: 1, comment: undefined, dataType: "BOOLEAN" },
|
|
122
|
+
{ name: "user-feedback", value: 1, comment: undefined, dataType: undefined },
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
it("degrades to the surviving sources when one query fails", async () => {
|
|
126
|
+
tracePages["user:org/me"] = [trace("mine", "2026-07-31T10:00:00Z", { userId: "org/me" })];
|
|
127
|
+
// The tag query throws; the reader's own runs must still render.
|
|
128
|
+
failFor = `${AGENT_OWNER_TAG}org`;
|
|
129
|
+
const rows = await listAdministeredTraces({
|
|
130
|
+
agentOwnerTags: ["org"],
|
|
131
|
+
ownerTag: "org/me",
|
|
132
|
+
});
|
|
133
|
+
expect(rows.map((r) => r.id)).toEqual(["mine"]);
|
|
134
|
+
});
|
|
135
|
+
it("throws when EVERY source fails, so the caller keeps its last good page", async () => {
|
|
136
|
+
// An empty array would read as "no traces yet" and wipe the feed.
|
|
137
|
+
failFor = "*";
|
|
138
|
+
await expect(listAdministeredTraces({ agentOwnerTags: ["org"], ownerTag: "org/me" })).rejects.toThrow("langfuse unavailable");
|
|
139
|
+
});
|
|
140
|
+
it("passes the agent filter to every source", async () => {
|
|
141
|
+
await listAdministeredTraces({
|
|
142
|
+
agentOwnerTags: ["org"],
|
|
143
|
+
ownerTag: "org/me",
|
|
144
|
+
agent: "my-agent",
|
|
145
|
+
});
|
|
146
|
+
for (const c of calls)
|
|
147
|
+
expect(c).toMatchObject({ name: "my-agent" });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
//# sourceMappingURL=reads.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reads.test.js","sourceRoot":"","sources":["../src/reads.test.ts"],"names":[],"mappings":"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"}
|
package/dist/scores.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface PostScoreInput {
|
|
2
|
+
/** The latch run the score belongs to (resolved to its trace id). */
|
|
3
|
+
runId: string;
|
|
4
|
+
/** Score name — e.g. a rubric name, or "user-feedback". */
|
|
5
|
+
name: string;
|
|
6
|
+
value: number;
|
|
7
|
+
/** Free-text rationale (judge reason / user comment). */
|
|
8
|
+
comment?: string;
|
|
9
|
+
dataType?: "NUMERIC" | "BOOLEAN" | "CATEGORICAL";
|
|
10
|
+
/** Extra metadata on the score — e.g. a human correction's original AI verdict. */
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* Stable score id. Defaults to a deterministic id derived from (runId, name)
|
|
14
|
+
* so repeated writes for the same run+name upsert one score rather than
|
|
15
|
+
* accumulating duplicates (live verdict → human correction overwrites it).
|
|
16
|
+
* Pass an explicit id only when you deliberately want independent scores.
|
|
17
|
+
*/
|
|
18
|
+
id?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The run's start time (epoch ms) — the stable per-run clock every writer for
|
|
21
|
+
* a run should pass. Langfuse only treats a write as an upsert when id, name
|
|
22
|
+
* AND timestamp (at date granularity) all match; the high-level SDK stamps
|
|
23
|
+
* `now`, so a correction on a later day would create a SECOND score and skew
|
|
24
|
+
* aggregates. Pinning every write for a run to its start keeps corrections
|
|
25
|
+
* idempotent across days. Defaults to now (same-day writes still dedupe).
|
|
26
|
+
*/
|
|
27
|
+
timestamp?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Langfuse environment for the score. Defaults to the environment tracing was
|
|
30
|
+
* set up with (so scores match their trace), then `LANGFUSE_TRACING_ENVIRONMENT`.
|
|
31
|
+
* Pass explicitly to override.
|
|
32
|
+
*/
|
|
33
|
+
environment?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Attach a score to a run's Langfuse trace (judge verdicts, user feedback).
|
|
37
|
+
* Sent through the raw ingestion endpoint (not the high-level `score.create`)
|
|
38
|
+
* so the event timestamp can be pinned to the run's clock — the only way to
|
|
39
|
+
* make cross-day corrections upsert instead of duplicate (see `timestamp`).
|
|
40
|
+
* Ingests immediately — callers are post-turn/endpoint paths, not hot loops —
|
|
41
|
+
* with a request-level timeout and no retries so a slow/down Langfuse can't
|
|
42
|
+
* hang a user-facing request for this non-critical write. Rejects on
|
|
43
|
+
* timeout/error; callers decide whether to surface or swallow it (the judge
|
|
44
|
+
* and endpoint callers all swallow).
|
|
45
|
+
*/
|
|
46
|
+
export declare function postScore(input: PostScoreInput & {
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
//# sourceMappingURL=scores.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scores.d.ts","sourceRoot":"","sources":["../src/scores.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;IACjD,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;OAKG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAUD;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAC7B,KAAK,EAAE,cAAc,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,OAAO,CAAC,IAAI,CAAC,CA0Cf"}
|
package/dist/scores.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getLangfuseClient } from "./client.js";
|
|
2
|
+
import { tracingEnvironment } from "./otel.js";
|
|
3
|
+
import { scoreIdForRun, traceIdForRun } from "./trace-id.js";
|
|
4
|
+
/** Default hard bound on the ingest network call (ms). */
|
|
5
|
+
const INGEST_TIMEOUT_MS = 10_000;
|
|
6
|
+
/**
|
|
7
|
+
* Attach a score to a run's Langfuse trace (judge verdicts, user feedback).
|
|
8
|
+
* Sent through the raw ingestion endpoint (not the high-level `score.create`)
|
|
9
|
+
* so the event timestamp can be pinned to the run's clock — the only way to
|
|
10
|
+
* make cross-day corrections upsert instead of duplicate (see `timestamp`).
|
|
11
|
+
* Ingests immediately — callers are post-turn/endpoint paths, not hot loops —
|
|
12
|
+
* with a request-level timeout and no retries so a slow/down Langfuse can't
|
|
13
|
+
* hang a user-facing request for this non-critical write. Rejects on
|
|
14
|
+
* timeout/error; callers decide whether to surface or swallow it (the judge
|
|
15
|
+
* and endpoint callers all swallow).
|
|
16
|
+
*/
|
|
17
|
+
export async function postScore(input) {
|
|
18
|
+
const client = getLangfuseClient();
|
|
19
|
+
const event = {
|
|
20
|
+
// Event id is unique per write so the ingestion event itself isn't deduped;
|
|
21
|
+
// the SCORE is deduped by `body.id` (the deterministic per-run+name id).
|
|
22
|
+
id: crypto.randomUUID(),
|
|
23
|
+
type: "score-create",
|
|
24
|
+
timestamp: new Date(input.timestamp ?? Date.now()).toISOString(),
|
|
25
|
+
body: {
|
|
26
|
+
id: input.id ?? (await scoreIdForRun(input.runId, input.name)),
|
|
27
|
+
traceId: await traceIdForRun(input.runId),
|
|
28
|
+
name: input.name,
|
|
29
|
+
value: input.value,
|
|
30
|
+
comment: input.comment,
|
|
31
|
+
dataType: input.dataType,
|
|
32
|
+
// Match the trace's environment (from ensureTracing) so scores land
|
|
33
|
+
// alongside their trace, not in a mismatched Langfuse environment; fall
|
|
34
|
+
// back to the env var on a cold, score-only invocation.
|
|
35
|
+
environment: input.environment ??
|
|
36
|
+
tracingEnvironment() ??
|
|
37
|
+
process.env.LANGFUSE_TRACING_ENVIRONMENT,
|
|
38
|
+
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
// Bound the call at the request level: this cancels the actual HTTP on
|
|
42
|
+
// timeout (a Promise.race would leave it running) and disables the SDK's
|
|
43
|
+
// default 2 retries, so the worst-case hang stays at one timeout window.
|
|
44
|
+
const res = await client.api.ingestion.batch({ batch: [event] }, {
|
|
45
|
+
timeoutInSeconds: Math.ceil((input.timeoutMs ?? INGEST_TIMEOUT_MS) / 1000),
|
|
46
|
+
maxRetries: 0,
|
|
47
|
+
});
|
|
48
|
+
if (res.errors.length > 0) {
|
|
49
|
+
throw new Error(`Langfuse score ingest failed: ${res.errors
|
|
50
|
+
.map((e) => e.message ?? String(e.status))
|
|
51
|
+
.join("; ")}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=scores.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scores.js","sourceRoot":"","sources":["../src/scores.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAqC7D,0DAA0D;AAC1D,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAOjC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAA8C;IAE9C,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;IACnC,MAAM,KAAK,GAAmB;QAC5B,4EAA4E;QAC5E,yEAAyE;QACzE,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE;QACvB,IAAI,EAAE,cAAc;QACpB,SAAS,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE;QAChE,IAAI,EAAE;YACJ,EAAE,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,MAAM,aAAa,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAC9D,OAAO,EAAE,MAAM,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC;YACzC,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,oEAAoE;YACpE,wEAAwE;YACxE,wDAAwD;YACxD,WAAW,EACT,KAAK,CAAC,WAAW;gBACjB,kBAAkB,EAAE;gBACpB,OAAO,CAAC,GAAG,CAAC,4BAA4B;YAC1C,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxD;KACF,CAAC;IACF,uEAAuE;IACvE,yEAAyE;IACzE,yEAAyE;IACzE,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAC1C,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,EAClB;QACE,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,SAAS,IAAI,iBAAiB,CAAC,GAAG,IAAI,CAAC;QAC1E,UAAU,EAAE,CAAC;KACd,CACF,CAAC;IACF,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,iCAAiC,GAAG,CAAC,MAAM;aACxC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;aACzC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChB,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { LatchTelemetry, TelemetryRunMeta, TelemetryRunOutcome } from "@intentface/latch-core";
|
|
2
|
+
export interface LangfuseTelemetryOptions<P> {
|
|
3
|
+
/**
|
|
4
|
+
* Tenant isolation tag derived from the principal (e.g. the platform's
|
|
5
|
+
* owner key). Becomes the trace's `userId` and an `owner:` tag — all
|
|
6
|
+
* platform reads filter on it, so it must be stable per tenant.
|
|
7
|
+
*/
|
|
8
|
+
ownerTag: (principal: P) => string;
|
|
9
|
+
/**
|
|
10
|
+
* The owner key of the AGENT being run, when the host can resolve one — becomes
|
|
11
|
+
* an `agent-owner:` tag. Distinct from `ownerTag` (who ran the turn): it lets a
|
|
12
|
+
* reader find every run of the agents it administers, including runs by other
|
|
13
|
+
* users. Return undefined for agents with no owner (e.g. code-registry ones),
|
|
14
|
+
* which simply omits the tag. Runs on the turn's hot path, so keep it cheap.
|
|
15
|
+
*/
|
|
16
|
+
agentOwnerTag?: (run: TelemetryRunMeta<P>) => Promise<string | undefined>;
|
|
17
|
+
/** Trace environment shown in Langfuse (default "development"). */
|
|
18
|
+
environment?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Post-trace extension point — the platform hangs the online judge here.
|
|
21
|
+
* Runs after the run's root span is finalized.
|
|
22
|
+
*/
|
|
23
|
+
afterRun?: (args: {
|
|
24
|
+
meta: TelemetryRunMeta<P>;
|
|
25
|
+
outcome: TelemetryRunOutcome;
|
|
26
|
+
traceId: string;
|
|
27
|
+
}) => Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Serverless background scheduler (e.g. Vercel's `waitUntil`). When set,
|
|
30
|
+
* `afterRun` is scheduled instead of awaited so it never delays the
|
|
31
|
+
* response; locally it's awaited inline.
|
|
32
|
+
*/
|
|
33
|
+
waitUntil?: (p: Promise<unknown>) => void;
|
|
34
|
+
/** Override enablement (default: keys present in env). */
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The one-liner Langfuse implementation of latch-core's telemetry seam.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* const runtime = createRuntime({
|
|
42
|
+
* ...,
|
|
43
|
+
* telemetry: isLangfuseConfigured()
|
|
44
|
+
* ? langfuseTelemetry({ ownerTag: (p) => ownerKey(p) })
|
|
45
|
+
* : undefined,
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* Every run becomes one Langfuse trace with a deterministic id derived from
|
|
50
|
+
* the runId (`traceIdForRun`), tagged `owner:` / `agent:` / `run:` (plus
|
|
51
|
+
* `agent-owner:` when `agentOwnerTag` resolves one) and scoped to
|
|
52
|
+
* `sessionId = chatId`, `userId = ownerTag`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function langfuseTelemetry<P>(opts: LangfuseTelemetryOptions<P>): LatchTelemetry<P>;
|
|
55
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACpB,MAAM,wBAAwB,CAAC;AAahC,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC;;;;OAIG;IACH,QAAQ,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,MAAM,CAAC;IACnC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC1E,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE;QAChB,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;QAC1B,OAAO,EAAE,mBAAmB,CAAC;QAC7B,OAAO,EAAE,MAAM,CAAC;KACjB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IAC1C,0DAA0D;IAC1D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAyBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,IAAI,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAChC,cAAc,CAAC,CAAC,CAAC,CAwHnB"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { LangfuseVercelAiSdkIntegration } from "@langfuse/vercel-ai-sdk";
|
|
2
|
+
import { propagateAttributes, startObservation, } from "@langfuse/tracing";
|
|
3
|
+
import { context as otelContext, trace as otelTrace } from "@opentelemetry/api";
|
|
4
|
+
import { ensureTracing, flushLangfuse } from "./otel.js";
|
|
5
|
+
import { isLangfuseConfigured } from "./client.js";
|
|
6
|
+
import { traceIdForRun } from "./trace-id.js";
|
|
7
|
+
/** A random 16-hex OTEL span id (parent seed for the deterministic trace). */
|
|
8
|
+
function randomSpanId() {
|
|
9
|
+
const bytes = crypto.getRandomValues(new Uint8Array(8));
|
|
10
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
11
|
+
}
|
|
12
|
+
/** Text of the last assistant message (for trace-level output). */
|
|
13
|
+
function lastAssistantText(messages) {
|
|
14
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
15
|
+
const m = messages[i];
|
|
16
|
+
if (m?.role !== "assistant")
|
|
17
|
+
continue;
|
|
18
|
+
const text = (m.parts ?? [])
|
|
19
|
+
.filter((p) => p.type === "text" && typeof p.text === "string")
|
|
20
|
+
.map((p) => p.text)
|
|
21
|
+
.join(" ")
|
|
22
|
+
.trim();
|
|
23
|
+
return text.length > 0 ? text : undefined;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The one-liner Langfuse implementation of latch-core's telemetry seam.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const runtime = createRuntime({
|
|
32
|
+
* ...,
|
|
33
|
+
* telemetry: isLangfuseConfigured()
|
|
34
|
+
* ? langfuseTelemetry({ ownerTag: (p) => ownerKey(p) })
|
|
35
|
+
* : undefined,
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Every run becomes one Langfuse trace with a deterministic id derived from
|
|
40
|
+
* the runId (`traceIdForRun`), tagged `owner:` / `agent:` / `run:` (plus
|
|
41
|
+
* `agent-owner:` when `agentOwnerTag` resolves one) and scoped to
|
|
42
|
+
* `sessionId = chatId`, `userId = ownerTag`.
|
|
43
|
+
*/
|
|
44
|
+
export function langfuseTelemetry(opts) {
|
|
45
|
+
const enabled = opts.enabled ?? isLangfuseConfigured();
|
|
46
|
+
const integration = new LangfuseVercelAiSdkIntegration();
|
|
47
|
+
// The run's root span stays open until onRunFinished so the trace gets a
|
|
48
|
+
// real duration + trace-level input/output. Keyed by runId (one per turn).
|
|
49
|
+
const openSpans = new Map();
|
|
50
|
+
return {
|
|
51
|
+
telemetryFor(run) {
|
|
52
|
+
if (!enabled)
|
|
53
|
+
return undefined;
|
|
54
|
+
return {
|
|
55
|
+
isEnabled: true,
|
|
56
|
+
functionId: `agent:${run.agent}`,
|
|
57
|
+
integrations: [integration],
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
async withRunSpan(run, fn) {
|
|
61
|
+
if (!enabled)
|
|
62
|
+
return fn();
|
|
63
|
+
ensureTracing({ environment: opts.environment ?? "development" });
|
|
64
|
+
const owner = opts.ownerTag(run.principal);
|
|
65
|
+
// Seed the root span with the deterministic trace id so scores and
|
|
66
|
+
// feedback can address the trace from the runId alone.
|
|
67
|
+
const span = startObservation(run.agent, {}, {
|
|
68
|
+
asType: "agent",
|
|
69
|
+
parentSpanContext: {
|
|
70
|
+
traceId: await traceIdForRun(run.runId),
|
|
71
|
+
spanId: randomSpanId(),
|
|
72
|
+
traceFlags: 1,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
openSpans.set(run.runId, span);
|
|
76
|
+
// Run the model call with the root span active: the AI SDK integration
|
|
77
|
+
// roots its spans in the active context, and propagated attributes
|
|
78
|
+
// (trace name/session/user/tags) flow to every child span.
|
|
79
|
+
const ctx = otelTrace.setSpan(otelContext.active(), span.otelSpan);
|
|
80
|
+
// Group a whole delegation tree under one session: subagent turns run in
|
|
81
|
+
// their own `sub_…` chat, so keying the session on the ROOT conversation
|
|
82
|
+
// (threaded down by the runtime) makes the main turn and its helpers show
|
|
83
|
+
// together in Langfuse. A `parent-run:` tag links each helper to the run
|
|
84
|
+
// that spawned it; `depth` (0 = top-level) records how deep it nested.
|
|
85
|
+
const sessionId = run.rootChatId ?? run.chatId;
|
|
86
|
+
const tags = [`owner:${owner}`, `agent:${run.agent}`, `run:${run.runId}`];
|
|
87
|
+
if (run.parentRunId)
|
|
88
|
+
tags.push(`parent-run:${run.parentRunId}`, "subagent");
|
|
89
|
+
// `agent-owner:` is how the observability feed finds runs of the agents a
|
|
90
|
+
// reader administers — including runs by OTHER users. Best-effort: a failed
|
|
91
|
+
// lookup must not cost the turn its trace, it only costs that filter.
|
|
92
|
+
if (opts.agentOwnerTag) {
|
|
93
|
+
try {
|
|
94
|
+
const agentOwner = await opts.agentOwnerTag(run);
|
|
95
|
+
if (agentOwner)
|
|
96
|
+
tags.push(`agent-owner:${agentOwner}`);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.warn(`agentOwnerTag failed (run ${run.runId}):`, e);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return otelContext.with(ctx, () => propagateAttributes({
|
|
103
|
+
traceName: run.agent,
|
|
104
|
+
sessionId,
|
|
105
|
+
userId: owner,
|
|
106
|
+
tags,
|
|
107
|
+
metadata: {
|
|
108
|
+
runId: run.runId,
|
|
109
|
+
chatId: run.chatId,
|
|
110
|
+
agent: run.agent,
|
|
111
|
+
model: run.modelId,
|
|
112
|
+
depth: String(run.depth),
|
|
113
|
+
...(run.parentRunId ? { parentRunId: run.parentRunId } : {}),
|
|
114
|
+
},
|
|
115
|
+
}, fn));
|
|
116
|
+
},
|
|
117
|
+
async onRunFinished(run, outcome) {
|
|
118
|
+
if (!enabled)
|
|
119
|
+
return;
|
|
120
|
+
const span = openSpans.get(run.runId);
|
|
121
|
+
if (span) {
|
|
122
|
+
openSpans.delete(run.runId);
|
|
123
|
+
const input = outcome.inputText;
|
|
124
|
+
const output = lastAssistantText(outcome.messages);
|
|
125
|
+
span.update({
|
|
126
|
+
input,
|
|
127
|
+
output,
|
|
128
|
+
level: outcome.status === "errored" ? "ERROR" : "DEFAULT",
|
|
129
|
+
statusMessage: outcome.error,
|
|
130
|
+
});
|
|
131
|
+
// Trace-level IO (what the trace list/preview shows).
|
|
132
|
+
span.setTraceIO({ input, output });
|
|
133
|
+
span.end();
|
|
134
|
+
}
|
|
135
|
+
if (opts.afterRun) {
|
|
136
|
+
const work = (async () => {
|
|
137
|
+
await opts.afterRun({
|
|
138
|
+
meta: run,
|
|
139
|
+
outcome,
|
|
140
|
+
traceId: await traceIdForRun(run.runId),
|
|
141
|
+
});
|
|
142
|
+
})();
|
|
143
|
+
if (opts.waitUntil)
|
|
144
|
+
opts.waitUntil(work.catch(() => { }));
|
|
145
|
+
else
|
|
146
|
+
await work;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
async flush() {
|
|
150
|
+
if (!enabled)
|
|
151
|
+
return;
|
|
152
|
+
const p = flushLangfuse();
|
|
153
|
+
// Keep the flush alive in serverless even when the caller can't await it
|
|
154
|
+
// (e.g. core's synchronous onError path voids the promise). waitUntil
|
|
155
|
+
// registers it with the platform; we still await for the normal path.
|
|
156
|
+
opts.waitUntil?.(p.catch(() => { }));
|
|
157
|
+
await p;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deterministic Langfuse trace id for a latch run. Both the tracer (span
|
|
3
|
+
* seeding) and out-of-band writers (judge scores, user feedback) derive the
|
|
4
|
+
* same id from the runId, so scores land on the run's trace without lookups.
|
|
5
|
+
*/
|
|
6
|
+
export declare function traceIdForRun(runId: string): Promise<string>;
|
|
7
|
+
/**
|
|
8
|
+
* A deterministic Langfuse score id for a (run, score-name) pair. Langfuse
|
|
9
|
+
* upserts a score only when its id, name AND timestamp (date granularity) all
|
|
10
|
+
* match, so deriving the id from run+name is necessary but not sufficient:
|
|
11
|
+
* writers must ALSO pin the score timestamp to the run's clock (see
|
|
12
|
+
* `PostScoreInput.timestamp`). Together they make score writes idempotent — the
|
|
13
|
+
* live judge and a later human correction of the same rubric write the SAME id
|
|
14
|
+
* and the correction overwrites the original verdict instead of leaving two
|
|
15
|
+
* scores of the same name on the trace, even across days.
|
|
16
|
+
*/
|
|
17
|
+
export declare function scoreIdForRun(runId: string, name: string): Promise<string>;
|
|
18
|
+
//# sourceMappingURL=trace-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-id.d.ts","sourceRoot":"","sources":["../src/trace-id.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}
|
package/dist/trace-id.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createTraceId } from "@langfuse/tracing";
|
|
2
|
+
/**
|
|
3
|
+
* The deterministic Langfuse trace id for a latch run. Both the tracer (span
|
|
4
|
+
* seeding) and out-of-band writers (judge scores, user feedback) derive the
|
|
5
|
+
* same id from the runId, so scores land on the run's trace without lookups.
|
|
6
|
+
*/
|
|
7
|
+
export function traceIdForRun(runId) {
|
|
8
|
+
return createTraceId(`latch-run:${runId}`);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A deterministic Langfuse score id for a (run, score-name) pair. Langfuse
|
|
12
|
+
* upserts a score only when its id, name AND timestamp (date granularity) all
|
|
13
|
+
* match, so deriving the id from run+name is necessary but not sufficient:
|
|
14
|
+
* writers must ALSO pin the score timestamp to the run's clock (see
|
|
15
|
+
* `PostScoreInput.timestamp`). Together they make score writes idempotent — the
|
|
16
|
+
* live judge and a later human correction of the same rubric write the SAME id
|
|
17
|
+
* and the correction overwrites the original verdict instead of leaving two
|
|
18
|
+
* scores of the same name on the trace, even across days.
|
|
19
|
+
*/
|
|
20
|
+
export function scoreIdForRun(runId, name) {
|
|
21
|
+
return createTraceId(`latch-score:${runId}:${name}`);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=trace-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-id.js","sourceRoot":"","sources":["../src/trace-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAO,aAAa,CAAC,aAAa,KAAK,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,IAAY;IACvD,OAAO,aAAa,CAAC,eAAe,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC;AACvD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intentface/latch-langfuse",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "Latch observability on Langfuse — one-line tracing for latch runtimes via the AI SDK telemetry integration, plus score and trace-read helpers for platforms.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"ai-sdk",
|
|
8
|
+
"agents",
|
|
9
|
+
"llm",
|
|
10
|
+
"langfuse",
|
|
11
|
+
"opentelemetry",
|
|
12
|
+
"observability"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"registry": "https://registry.npmjs.org"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@langfuse/client": "^5.9.1",
|
|
35
|
+
"@langfuse/otel": "^5.9.1",
|
|
36
|
+
"@langfuse/tracing": "^5.9.1",
|
|
37
|
+
"@langfuse/vercel-ai-sdk": "^5.9.1",
|
|
38
|
+
"@opentelemetry/api": "^1.9.0",
|
|
39
|
+
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
40
|
+
"@intentface/latch-core": "^0.9.1"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"ai": ">=7.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22",
|
|
47
|
+
"ai": "^7.0.2",
|
|
48
|
+
"typescript": "^5.7.0",
|
|
49
|
+
"vitest": "^4"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/Intentface/intentface-latch.git",
|
|
54
|
+
"directory": "packages/langfuse"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.json",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
}
|
|
61
|
+
}
|