@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 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. |
@@ -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"}
@@ -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
@@ -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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=reads.test.d.ts.map
@@ -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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"}
@@ -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":"AAMA,OAAO,EAAE,8BAA8B,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EACL,mBAAmB,EACnB,gBAAgB,GAEjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAsC9C,8EAA8E;AAC9E,SAAS,YAAY;IACnB,MAAM,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IACxD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,mEAAmE;AACnE,SAAS,iBAAiB,CAAC,QAAmB;IAC5C,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9C,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAEP,CAAC;QACd,IAAI,CAAC,EAAE,IAAI,KAAK,WAAW;YAAE,SAAS;QACtC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;aACzB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC;aAC9D,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;aAClB,IAAI,CAAC,GAAG,CAAC;aACT,IAAI,EAAE,CAAC;QACV,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAiC;IAEjC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,oBAAoB,EAAE,CAAC;IACvD,MAAM,WAAW,GAAG,IAAI,8BAA8B,EAAE,CAAC;IACzD,yEAAyE;IACzE,2EAA2E;IAC3E,MAAM,SAAS,GAAG,IAAI,GAAG,EAAyB,CAAC;IAEnD,OAAO;QACL,YAAY,CAAC,GAAG;YACd,IAAI,CAAC,OAAO;gBAAE,OAAO,SAAS,CAAC;YAC/B,OAAO;gBACL,SAAS,EAAE,IAAI;gBACf,UAAU,EAAE,SAAS,GAAG,CAAC,KAAK,EAAE;gBAChC,YAAY,EAAE,CAAC,WAAW,CAAC;aAC5B,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE;YACvB,IAAI,CAAC,OAAO;gBAAE,OAAO,EAAE,EAAE,CAAC;YAC1B,aAAa,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,aAAa,EAAE,CAAC,CAAC;YAClE,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAC3C,mEAAmE;YACnE,uDAAuD;YACvD,MAAM,IAAI,GAAG,gBAAgB,CAC3B,GAAG,CAAC,KAAK,EACT,EAAE,EACF;gBACE,MAAM,EAAE,OAAO;gBACf,iBAAiB,EAAE;oBACjB,OAAO,EAAE,MAAM,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC;oBACvC,MAAM,EAAE,YAAY,EAAE;oBACtB,UAAU,EAAE,CAAC;iBACd;aACF,CACF,CAAC;YACF,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC/B,uEAAuE;YACvE,mEAAmE;YACnE,2DAA2D;YAC3D,MAAM,GAAG,GAAG,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;YACnE,yEAAyE;YACzE,yEAAyE;YACzE,0EAA0E;YAC1E,yEAAyE;YACzE,uEAAuE;YACvE,MAAM,SAAS,GAAG,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,MAAM,CAAC;YAC/C,MAAM,IAAI,GAAG,CAAC,SAAS,KAAK,EAAE,EAAE,SAAS,GAAG,CAAC,KAAK,EAAE,EAAE,OAAO,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;YAC1E,IAAI,GAAG,CAAC,WAAW;gBAAE,IAAI,CAAC,IAAI,CAAC,cAAc,GAAG,CAAC,WAAW,EAAE,EAAE,UAAU,CAAC,CAAC;YAC5E,0EAA0E;YAC1E,4EAA4E;YAC5E,sEAAsE;YACtE,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBACvB,IAAI,CAAC;oBACH,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;oBACjD,IAAI,UAAU;wBAAE,IAAI,CAAC,IAAI,CAAC,eAAe,UAAU,EAAE,CAAC,CAAC;gBACzD,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,OAAO,CAAC,IAAI,CAAC,6BAA6B,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;gBAC9D,CAAC;YACH,CAAC;YACD,OAAO,WAAW,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAChC,mBAAmB,CACjB;gBACE,SAAS,EAAE,GAAG,CAAC,KAAK;gBACpB,SAAS;gBACT,MAAM,EAAE,KAAK;gBACb,IAAI;gBACJ,QAAQ,EAAE;oBACR,KAAK,EAAE,GAAG,CAAC,KAAK;oBAChB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,KAAK,EAAE,GAAG,CAAC,KAAK;oBAChB,KAAK,EAAE,GAAG,CAAC,OAAO;oBAClB,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;oBACxB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAC7D;aACF,EACD,EAAE,CACH,CACF,CAAC;QACJ,CAAC;QAED,KAAK,CAAC,aAAa,CAAC,GAAG,EAAE,OAAO;YAC9B,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACtC,IAAI,IAAI,EAAE,CAAC;gBACT,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC;gBAChC,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACnD,IAAI,CAAC,MAAM,CAAC;oBACV,KAAK;oBACL,MAAM;oBACN,KAAK,EAAE,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;oBACzD,aAAa,EAAE,OAAO,CAAC,KAAK;iBAC7B,CAAC,CAAC;gBACH,sDAAsD;gBACtD,IAAI,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;gBACnC,IAAI,CAAC,GAAG,EAAE,CAAC;YACb,CAAC;YACD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClB,MAAM,IAAI,GAAG,CAAC,KAAK,IAAI,EAAE;oBACvB,MAAM,IAAI,CAAC,QAAS,CAAC;wBACnB,IAAI,EAAE,GAAG;wBACT,OAAO;wBACP,OAAO,EAAE,MAAM,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC;qBACxC,CAAC,CAAC;gBACL,CAAC,CAAC,EAAE,CAAC;gBACL,IAAI,IAAI,CAAC,SAAS;oBAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC;;oBACpD,MAAM,IAAI,CAAC;YAClB,CAAC;QACH,CAAC;QAED,KAAK,CAAC,KAAK;YACT,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,MAAM,CAAC,GAAG,aAAa,EAAE,CAAC;YAC1B,yEAAyE;YACzE,sEAAsE;YACtE,sEAAsE;YACtE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC;YACpC,MAAM,CAAC,CAAC;QACV,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -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"}
@@ -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
+ }