@intentface/latch-otel 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,67 @@
1
+ # @intentface/latch-otel
2
+
3
+ Tracing for Latch over OpenTelemetry. Implements `@intentface/latch-core`'s
4
+ `telemetry` hook: each run is one trace, the AI SDK's model and tool spans nest
5
+ under it, and everything goes over OTLP to the backend you choose. A preset for
6
+ Langfuse is included.
7
+
8
+ ```bash
9
+ pnpm add @intentface/latch-otel @ai-sdk/otel # @ai-sdk/otel at the version matching your `ai`
10
+ ```
11
+
12
+ ## Langfuse
13
+
14
+ ```ts
15
+ import { createRuntime } from "@intentface/latch-core";
16
+ import { otelTelemetry } from "@intentface/latch-otel";
17
+ import { isLangfuseConfigured, langfuse } from "@intentface/latch-otel/langfuse";
18
+
19
+ const runtime = createRuntime({
20
+ // …storage, agents…
21
+ telemetry: isLangfuseConfigured()
22
+ ? otelTelemetry({ user: (p) => ownerKey(p), export: langfuse(), environment: "production" })
23
+ : undefined,
24
+ });
25
+ ```
26
+
27
+ `langfuse()` reads `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` and
28
+ `LANGFUSE_BASE_URL` (default `https://cloud.langfuse.com`), and sends to the
29
+ project's OTLP endpoint. Each run shows as one trace named after the agent, in
30
+ the conversation's session, with the user, tags (`owner:`, `agent:`, `run:`,
31
+ `agent-version:`, `parent-run:`, `agent-owner:`) and run metadata. Under it:
32
+ the AI SDK's agent span, each step, each model call as a generation with its
33
+ messages and token usage, and each tool call with its arguments and result.
34
+
35
+ ## Any other backend
36
+
37
+ ```ts
38
+ telemetry: otelTelemetry({ user: (p) => ownerKey(p) }),
39
+ ```
40
+
41
+ With no `export`, spans go over OTLP/HTTP to `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
42
+ or `OTEL_EXPORTER_OTLP_ENDPOINT`, with `OTEL_EXPORTER_OTLP_HEADERS`: Grafana,
43
+ Honeycomb, Datadog, Jaeger, a collector. Or pass any `SpanExporter`, or a
44
+ destination of your own (`{ exporter, runAttributes, finishedAttributes }`, the
45
+ shape `langfuse()` returns) to add the attributes your backend wants.
46
+
47
+ ## What's on the spans
48
+
49
+ - The run's span, `latch.run <agent>`: the whole turn, saving it included.
50
+ Its trace id is `traceIdForRun(runId)`, so anything that knows a run can link
51
+ to its trace. When the run ends it gets `input.value`, `output.value`,
52
+ `latch.run.status`, token usage and `latch.cost`, and an error status if the
53
+ run failed.
54
+ - On every span of the run: `session.id` (the conversation; a subagent shares
55
+ its parent's), `user.id`, `gen_ai.agent.name`, `latch.run.id`,
56
+ `latch.chat.id`, `latch.agent.version`, `latch.parent_run.id`, `latch.depth`,
57
+ `latch.trigger`, `deployment.environment.name`.
58
+ - The AI SDK's spans, in the GenAI semantic conventions, from `@ai-sdk/otel`
59
+ (configure it with `aiSdk`).
60
+
61
+ It runs its own tracer provider and registers an async context manager only if
62
+ the app has none, so it doesn't take over tracing your app already does. On
63
+ serverless, pass `waitUntil` so the flush outlives the response.
64
+
65
+ Replaces `@intentface/latch-langfuse` (0.21 and earlier), whose trace ids it
66
+ keeps. Reading traces back and writing scores were left out: they belong to
67
+ your app or an evaluation tool.
@@ -0,0 +1,3 @@
1
+ export { otelTelemetry, type OtelTelemetryOptions, type OtelDestination, type OtelRun, } from "./telemetry.js";
2
+ export { traceIdForRun } from "./trace-id.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,OAAO,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { otelTelemetry, } from "./telemetry.js";
2
+ export { traceIdForRun } from "./trace-id.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,GAId,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,38 @@
1
+ import type { SpanExporter } from "@opentelemetry/sdk-trace-base";
2
+ import type { OtelDestination } from "./telemetry.js";
3
+ export interface LangfuseOptions {
4
+ /** Default `LANGFUSE_PUBLIC_KEY`. */
5
+ publicKey?: string;
6
+ /** Default `LANGFUSE_SECRET_KEY`. */
7
+ secretKey?: string;
8
+ /** Default `LANGFUSE_BASE_URL`, else Langfuse Cloud EU (`https://cloud.langfuse.com`). */
9
+ baseUrl?: string;
10
+ /** Langfuse's environment for these traces. Default: `otelTelemetry`'s `environment`. */
11
+ environment?: string;
12
+ /** Send through this exporter instead of the OTLP one `langfuseOtlp` describes (a proxy, a test). */
13
+ exporter?: SpanExporter;
14
+ }
15
+ /** Whether the Langfuse keys are in the environment: `langfuse()` with no arguments would work. */
16
+ export declare function isLangfuseConfigured(): boolean;
17
+ /** Langfuse's OTLP endpoint and headers, for an exporter of your own. */
18
+ export declare function langfuseOtlp(opts?: Pick<LangfuseOptions, "publicKey" | "secretKey" | "baseUrl">): {
19
+ url: string;
20
+ headers: Record<string, string>;
21
+ };
22
+ /**
23
+ * Send traces to Langfuse, over its OTLP endpoint. Each run is one trace named
24
+ * after the agent, in the conversation's session, with the user, tags
25
+ * (`owner:`, `agent:`, `run:`, `agent-version:`, `parent-run:`,
26
+ * `agent-owner:`) and run metadata Langfuse filters on. The run's own span is
27
+ * an agent observation, marked as an error when the run errored.
28
+ *
29
+ * ```ts
30
+ * otelTelemetry({ user: (p) => ownerKey(p), export: langfuse() })
31
+ * ```
32
+ *
33
+ * Reads `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` and `LANGFUSE_BASE_URL`;
34
+ * throws without the keys (check `isLangfuseConfigured()` first if they're
35
+ * optional).
36
+ */
37
+ export declare function langfuse(opts?: LangfuseOptions): OtelDestination;
38
+ //# sourceMappingURL=langfuse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"langfuse.d.ts","sourceRoot":"","sources":["../src/langfuse.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAW,MAAM,gBAAgB,CAAC;AAE/D,MAAM,WAAW,eAAe;IAC9B,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qGAAqG;IACrG,QAAQ,CAAC,EAAE,YAAY,CAAC;CACzB;AAID,mGAAmG;AACnG,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,IAAI,GAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,WAAW,GAAG,SAAS,CAAM,GAAG;IACrG,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC,CAeA;AAYD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,eAAe,CAgCpE"}
@@ -0,0 +1,86 @@
1
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
2
+ const env = (name) => (typeof process === "undefined" ? undefined : process.env[name]) || undefined;
3
+ /** Whether the Langfuse keys are in the environment: `langfuse()` with no arguments would work. */
4
+ export function isLangfuseConfigured() {
5
+ return Boolean(env("LANGFUSE_PUBLIC_KEY") && env("LANGFUSE_SECRET_KEY"));
6
+ }
7
+ /** Langfuse's OTLP endpoint and headers, for an exporter of your own. */
8
+ export function langfuseOtlp(opts = {}) {
9
+ const publicKey = opts.publicKey ?? env("LANGFUSE_PUBLIC_KEY");
10
+ const secretKey = opts.secretKey ?? env("LANGFUSE_SECRET_KEY");
11
+ if (!publicKey || !secretKey) {
12
+ throw new Error("langfuse(): set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY, or pass publicKey and secretKey");
13
+ }
14
+ const baseUrl = (opts.baseUrl ?? env("LANGFUSE_BASE_URL") ?? "https://cloud.langfuse.com").replace(/\/+$/, "");
15
+ return {
16
+ url: `${baseUrl}/api/public/otel/v1/traces`,
17
+ headers: {
18
+ Authorization: `Basic ${btoa(`${publicKey}:${secretKey}`)}`,
19
+ // Ingest as spans arrive rather than in Langfuse's batch path.
20
+ "x-langfuse-ingestion-version": "4",
21
+ },
22
+ };
23
+ }
24
+ /** The tags a run's trace gets: what Langfuse's trace list filters on. */
25
+ function tagsOf(run) {
26
+ const tags = [`agent:${run.agent}`, `run:${run.runId}`];
27
+ if (run.user)
28
+ tags.unshift(`owner:${run.user}`);
29
+ if (run.parentRunId)
30
+ tags.push(`parent-run:${run.parentRunId}`, "subagent");
31
+ if (run.agentVersion != null)
32
+ tags.push(`agent-version:${run.agentVersion}`);
33
+ if (run.agentOwner)
34
+ tags.push(`agent-owner:${run.agentOwner}`);
35
+ return tags;
36
+ }
37
+ /**
38
+ * Send traces to Langfuse, over its OTLP endpoint. Each run is one trace named
39
+ * after the agent, in the conversation's session, with the user, tags
40
+ * (`owner:`, `agent:`, `run:`, `agent-version:`, `parent-run:`,
41
+ * `agent-owner:`) and run metadata Langfuse filters on. The run's own span is
42
+ * an agent observation, marked as an error when the run errored.
43
+ *
44
+ * ```ts
45
+ * otelTelemetry({ user: (p) => ownerKey(p), export: langfuse() })
46
+ * ```
47
+ *
48
+ * Reads `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` and `LANGFUSE_BASE_URL`;
49
+ * throws without the keys (check `isLangfuseConfigured()` first if they're
50
+ * optional).
51
+ */
52
+ export function langfuse(opts = {}) {
53
+ const exporter = opts.exporter ?? new OTLPTraceExporter(langfuseOtlp(opts));
54
+ return {
55
+ exporter,
56
+ runAttributes(run) {
57
+ const environment = opts.environment ?? run.environment;
58
+ return {
59
+ everySpan: {
60
+ "langfuse.trace.name": run.agent,
61
+ "langfuse.session.id": run.sessionId,
62
+ ...(run.user ? { "langfuse.user.id": run.user } : {}),
63
+ "langfuse.trace.tags": tagsOf(run),
64
+ "langfuse.trace.metadata.runId": run.runId,
65
+ "langfuse.trace.metadata.chatId": run.chatId,
66
+ "langfuse.trace.metadata.agent": run.agent,
67
+ "langfuse.trace.metadata.model": run.modelId,
68
+ "langfuse.trace.metadata.depth": String(run.depth),
69
+ ...(run.agentVersion != null ? { "langfuse.trace.metadata.agentVersion": String(run.agentVersion) } : {}),
70
+ ...(run.parentRunId ? { "langfuse.trace.metadata.parentRunId": run.parentRunId } : {}),
71
+ ...(environment ? { "langfuse.environment": environment } : {}),
72
+ },
73
+ root: { "langfuse.observation.type": "agent" },
74
+ };
75
+ },
76
+ finishedAttributes(_run, outcome) {
77
+ if (outcome.status !== "errored")
78
+ return undefined;
79
+ return {
80
+ "langfuse.observation.level": "ERROR",
81
+ ...(outcome.error ? { "langfuse.observation.status_message": outcome.error } : {}),
82
+ };
83
+ },
84
+ };
85
+ }
86
+ //# sourceMappingURL=langfuse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"langfuse.js","sourceRoot":"","sources":["../src/langfuse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ import type { LatchTelemetry, TelemetryRunMeta, TelemetryRunOutcome, TurnTrigger } from "@intentface/latch-core";
2
+ import { type OpenTelemetryOptions } from "@ai-sdk/otel";
3
+ import { type Attributes } from "@opentelemetry/api";
4
+ import { type SpanExporter } from "@opentelemetry/sdk-trace-base";
5
+ /** What a run's spans are labelled with: what latch knows about the run when it starts. */
6
+ export interface OtelRun {
7
+ runId: string;
8
+ chatId: string;
9
+ /** The top-level conversation: a subagent's run shares it with the run that spawned it. */
10
+ sessionId: string;
11
+ agent: string;
12
+ modelId: string;
13
+ /** 0 for a top-level turn, more for a subagent. */
14
+ depth: number;
15
+ trigger: TurnTrigger;
16
+ /** The stored agent version behind the run, for agents that come from a store. */
17
+ agentVersion?: number;
18
+ /** The run that spawned this one, for a subagent. */
19
+ parentRunId?: string;
20
+ /** From `user`. */
21
+ user?: string;
22
+ /** From `agentOwner`. */
23
+ agentOwner?: string;
24
+ /** From `environment`. */
25
+ environment?: string;
26
+ }
27
+ /**
28
+ * Where spans go, and anything that backend wants on them beyond the standard
29
+ * attributes. `langfuse()` from `@intentface/latch-otel/langfuse` is one.
30
+ */
31
+ export interface OtelDestination {
32
+ exporter: SpanExporter;
33
+ /** Attributes for every span of a run (`everySpan`), and for the run's own span (`root`). */
34
+ runAttributes?(run: OtelRun): {
35
+ everySpan?: Attributes;
36
+ root?: Attributes;
37
+ };
38
+ /** Attributes for the run's own span once the run has finished. */
39
+ finishedAttributes?(run: OtelRun, outcome: TelemetryRunOutcome): Attributes | undefined;
40
+ }
41
+ export interface OtelTelemetryOptions<P> {
42
+ /**
43
+ * Where spans go: an exporter, or a destination such as `langfuse()`.
44
+ * Default: OTLP over HTTP to `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` (or
45
+ * `OTEL_EXPORTER_OTLP_ENDPOINT`), with `OTEL_EXPORTER_OTLP_HEADERS`.
46
+ */
47
+ export?: SpanExporter | OtelDestination;
48
+ /**
49
+ * Who a run is for, as one stable string (usually the owner key). Becomes
50
+ * `user.id`. Leave it out to record no user.
51
+ */
52
+ user?: (principal: P) => string;
53
+ /**
54
+ * Who owns the agent being run, when that's someone other than the user (an
55
+ * org's shared agent). Becomes `latch.agent_owner`. Runs before every turn,
56
+ * so keep it cheap; a throw only loses the attribute.
57
+ */
58
+ agentOwner?: (run: TelemetryRunMeta<P>) => Promise<string | undefined>;
59
+ /** `deployment.environment.name`, e.g. "production". */
60
+ environment?: string;
61
+ /** More attributes for every span of a run. */
62
+ attributes?: (run: TelemetryRunMeta<P>) => Attributes | undefined;
63
+ /** Options for the AI SDK's spans (`@ai-sdk/otel`), e.g. `{ usage: true }`. */
64
+ aiSdk?: Omit<OpenTelemetryOptions, "tracer">;
65
+ /**
66
+ * Keeps the flush alive after the response on serverless (Vercel's
67
+ * `waitUntil`). Without it, flush is only awaited.
68
+ */
69
+ waitUntil?: (promise: Promise<unknown>) => void;
70
+ /** Default true. False turns every hook into a no-op. */
71
+ enabled?: boolean;
72
+ }
73
+ /**
74
+ * Latch's telemetry hook over OpenTelemetry. Each run is one trace, its id
75
+ * derived from the run id (`traceIdForRun`), rooted in a `latch.run <agent>`
76
+ * span: the whole turn, saving it included. The AI SDK's own spans (its
77
+ * `invoke_agent`, each step, each model call and tool call, in the GenAI
78
+ * semantic conventions, from `@ai-sdk/otel`) nest under it. Every span carries the session, the user and
79
+ * the run, so a backend can filter on any of them.
80
+ *
81
+ * ```ts
82
+ * import { otelTelemetry } from "@intentface/latch-otel";
83
+ * import { langfuse } from "@intentface/latch-otel/langfuse";
84
+ *
85
+ * createRuntime({
86
+ * ...,
87
+ * telemetry: otelTelemetry({ user: (p) => ownerKey(p), export: langfuse() }),
88
+ * });
89
+ * ```
90
+ *
91
+ * It runs its own tracer provider, so it neither needs nor replaces one your
92
+ * app registered. Add `@ai-sdk/otel` at the version matching your `ai`.
93
+ */
94
+ export declare function otelTelemetry<P>(opts?: OtelTelemetryOptions<P>): LatchTelemetry<P>;
95
+ //# 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,EAAE,cAAc,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACjH,OAAO,EAAiB,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAkC,KAAK,UAAU,EAA2B,MAAM,oBAAoB,CAAC;AAG9G,OAAO,EAKL,KAAK,YAAY,EAElB,MAAM,+BAA+B,CAAC;AAGvC,2FAA2F;AAC3F,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,2FAA2F;IAC3F,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;IACrB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,YAAY,CAAC;IACvB,6FAA6F;IAC7F,aAAa,CAAC,CAAC,GAAG,EAAE,OAAO,GAAG;QAAE,SAAS,CAAC,EAAE,UAAU,CAAC;QAAC,IAAI,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;IAC5E,mEAAmE;IACnE,kBAAkB,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,mBAAmB,GAAG,UAAU,GAAG,SAAS,CAAC;CACzF;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,GAAG,eAAe,CAAC;IACxC;;;OAGG;IACH,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,MAAM,CAAC;IAChC;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvE,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,UAAU,GAAG,SAAS,CAAC;IAClE,+EAA+E;IAC/E,KAAK,CAAC,EAAE,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC,CAAC;IAC7C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IAChD,yDAAyD;IACzD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA+DD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,GAAE,oBAAoB,CAAC,CAAC,CAAM,GAAG,cAAc,CAAC,CAAC,CAAC,CAgItF"}
@@ -0,0 +1,214 @@
1
+ import { OpenTelemetry } from "@ai-sdk/otel";
2
+ import { context, SpanStatusCode, trace } from "@opentelemetry/api";
3
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
4
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
5
+ import { BasicTracerProvider, BatchSpanProcessor, RandomIdGenerator, } from "@opentelemetry/sdk-trace-base";
6
+ import { traceIdForRun } from "./trace-id.js";
7
+ /** Run attributes carried in the context, copied onto every span the run starts. */
8
+ const RUN_ATTRIBUTES = Symbol.for("latch-otel.run-attributes");
9
+ let contextManagerSet = false;
10
+ /**
11
+ * Spans only nest across `await`s with an async context manager. Register one
12
+ * unless the app already has: `setGlobalContextManager` refuses a second.
13
+ */
14
+ function ensureContextManager() {
15
+ if (contextManagerSet)
16
+ return;
17
+ contextManagerSet = true;
18
+ context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());
19
+ }
20
+ /** Text of the last assistant message: the run's output. */
21
+ function lastAssistantText(messages) {
22
+ for (let i = messages.length - 1; i >= 0; i--) {
23
+ const m = messages[i];
24
+ if (m?.role !== "assistant")
25
+ continue;
26
+ const text = (m.parts ?? [])
27
+ .filter((p) => p.type === "text" && typeof p.text === "string")
28
+ .map((p) => p.text)
29
+ .join(" ")
30
+ .trim();
31
+ return text || undefined;
32
+ }
33
+ return undefined;
34
+ }
35
+ /** A host callback's result, or undefined (and a warning) when it throws: telemetry never fails a turn. */
36
+ function guarded(label, runId, f) {
37
+ try {
38
+ return f();
39
+ }
40
+ catch (e) {
41
+ console.warn(`latch-otel: ${label} failed (run ${runId}):`, e);
42
+ return undefined;
43
+ }
44
+ }
45
+ const defined = (a) => Object.fromEntries(Object.entries(a).filter(([, v]) => v !== undefined));
46
+ /** The standard attributes, on every span of a run. */
47
+ function standardAttributes(run) {
48
+ return defined({
49
+ "session.id": run.sessionId,
50
+ "user.id": run.user,
51
+ "gen_ai.conversation.id": run.sessionId,
52
+ "gen_ai.agent.name": run.agent,
53
+ "deployment.environment.name": run.environment,
54
+ "latch.run.id": run.runId,
55
+ "latch.chat.id": run.chatId,
56
+ "latch.agent": run.agent,
57
+ "latch.agent.version": run.agentVersion,
58
+ "latch.agent_owner": run.agentOwner,
59
+ "latch.parent_run.id": run.parentRunId,
60
+ "latch.depth": run.depth,
61
+ "latch.trigger": run.trigger,
62
+ });
63
+ }
64
+ /**
65
+ * Latch's telemetry hook over OpenTelemetry. Each run is one trace, its id
66
+ * derived from the run id (`traceIdForRun`), rooted in a `latch.run <agent>`
67
+ * span: the whole turn, saving it included. The AI SDK's own spans (its
68
+ * `invoke_agent`, each step, each model call and tool call, in the GenAI
69
+ * semantic conventions, from `@ai-sdk/otel`) nest under it. Every span carries the session, the user and
70
+ * the run, so a backend can filter on any of them.
71
+ *
72
+ * ```ts
73
+ * import { otelTelemetry } from "@intentface/latch-otel";
74
+ * import { langfuse } from "@intentface/latch-otel/langfuse";
75
+ *
76
+ * createRuntime({
77
+ * ...,
78
+ * telemetry: otelTelemetry({ user: (p) => ownerKey(p), export: langfuse() }),
79
+ * });
80
+ * ```
81
+ *
82
+ * It runs its own tracer provider, so it neither needs nor replaces one your
83
+ * app registered. Add `@ai-sdk/otel` at the version matching your `ai`.
84
+ */
85
+ export function otelTelemetry(opts = {}) {
86
+ if (opts.enabled === false)
87
+ return {};
88
+ const destination = opts.export && "exporter" in opts.export ? opts.export : { exporter: opts.export ?? new OTLPTraceExporter() };
89
+ // The next root span's trace id. Set right before `startSpan` and consumed by
90
+ // it, synchronously, so a root span gets the run's id and nothing else does.
91
+ let nextTraceId;
92
+ const random = new RandomIdGenerator();
93
+ const idGenerator = {
94
+ generateTraceId: () => {
95
+ const id = nextTraceId ?? random.generateTraceId();
96
+ nextTraceId = undefined;
97
+ return id;
98
+ },
99
+ generateSpanId: () => random.generateSpanId(),
100
+ };
101
+ const propagate = {
102
+ onStart(span, parent) {
103
+ const attributes = parent.getValue(RUN_ATTRIBUTES);
104
+ if (attributes)
105
+ span.setAttributes(attributes);
106
+ },
107
+ onEnd() { },
108
+ forceFlush: async () => { },
109
+ shutdown: async () => { },
110
+ };
111
+ const provider = new BasicTracerProvider({
112
+ idGenerator,
113
+ spanProcessors: [propagate, new BatchSpanProcessor(destination.exporter)],
114
+ });
115
+ const tracer = provider.getTracer("@intentface/latch-otel");
116
+ const aiSdk = new OpenTelemetry({ ...opts.aiSdk, tracer });
117
+ // The run's span stays open until the run finishes, for a real duration and its output.
118
+ const open = new Map();
119
+ return {
120
+ telemetryFor(run) {
121
+ return { isEnabled: true, functionId: `agent:${run.agent}`, integrations: [aiSdk] };
122
+ },
123
+ async withRunSpan(meta, fn) {
124
+ // Core awaits this unguarded (it has to run `fn`), so nothing before
125
+ // `fn` may throw: a failing callback loses its attributes, anything else
126
+ // loses the span, and the turn runs either way.
127
+ let ctx;
128
+ try {
129
+ ctx = await startRun(meta);
130
+ }
131
+ catch (e) {
132
+ console.warn(`latch-otel: couldn't start the run's span (run ${meta.runId}):`, e);
133
+ }
134
+ return ctx ? context.with(ctx, fn) : fn();
135
+ },
136
+ async onRunFinished(meta, outcome) {
137
+ const entry = open.get(meta.runId);
138
+ if (!entry)
139
+ return;
140
+ open.delete(meta.runId);
141
+ const { span, run } = entry;
142
+ try {
143
+ span.setAttributes(defined({
144
+ "input.value": outcome.inputText,
145
+ "output.value": lastAssistantText(outcome.messages),
146
+ "latch.run.status": outcome.status,
147
+ "latch.usage.input_tokens": outcome.usage.inputTokens,
148
+ "latch.usage.output_tokens": outcome.usage.outputTokens,
149
+ "latch.usage.cache_read_tokens": outcome.usage.cacheReadTokens,
150
+ "latch.usage.cache_write_tokens": outcome.usage.cacheWriteTokens,
151
+ "latch.cost": outcome.cost,
152
+ }));
153
+ if (outcome.status === "errored")
154
+ span.setStatus({ code: SpanStatusCode.ERROR, message: outcome.error });
155
+ const finished = guarded("finishedAttributes", meta.runId, () => destination.finishedAttributes?.(run, outcome));
156
+ if (finished)
157
+ span.setAttributes(finished);
158
+ }
159
+ finally {
160
+ span.end();
161
+ }
162
+ },
163
+ async flush() {
164
+ const done = provider.forceFlush();
165
+ opts.waitUntil?.(done.catch(() => { }));
166
+ await done;
167
+ },
168
+ };
169
+ /** Open the run's span; the context the run then runs in. */
170
+ async function startRun(meta) {
171
+ ensureContextManager();
172
+ let agentOwner;
173
+ try {
174
+ agentOwner = await opts.agentOwner?.(meta);
175
+ }
176
+ catch (e) {
177
+ console.warn(`latch-otel: agentOwner failed (run ${meta.runId}):`, e);
178
+ }
179
+ const run = {
180
+ runId: meta.runId,
181
+ chatId: meta.chatId,
182
+ sessionId: meta.rootChatId ?? meta.chatId,
183
+ agent: meta.agent,
184
+ modelId: meta.modelId,
185
+ depth: meta.depth,
186
+ trigger: meta.trigger,
187
+ agentVersion: meta.agentVersion,
188
+ parentRunId: meta.parentRunId,
189
+ user: opts.user ? guarded("user", meta.runId, () => opts.user(meta.principal)) : undefined,
190
+ agentOwner,
191
+ environment: opts.environment,
192
+ };
193
+ const extra = guarded("runAttributes", meta.runId, () => destination.runAttributes?.(run));
194
+ const everySpan = {
195
+ ...standardAttributes(run),
196
+ ...guarded("attributes", meta.runId, () => opts.attributes?.(meta)),
197
+ ...extra?.everySpan,
198
+ };
199
+ nextTraceId = await traceIdForRun(meta.runId);
200
+ let span;
201
+ try {
202
+ span = tracer.startSpan(`latch.run ${meta.agent}`, {
203
+ root: true,
204
+ attributes: { ...everySpan, "latch.model": meta.modelId, ...extra?.root },
205
+ });
206
+ }
207
+ finally {
208
+ nextTraceId = undefined;
209
+ }
210
+ open.set(meta.runId, { span, run });
211
+ return trace.setSpan(context.active().setValue(RUN_ATTRIBUTES, everySpan), span);
212
+ }
213
+ }
214
+ //# 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,11 @@
1
+ /**
2
+ * The trace id a latch run is recorded under: derived from the run id, so
3
+ * anything that knows the run (a feedback button, a later score writer, a link
4
+ * from your own UI) can address its trace without looking it up.
5
+ *
6
+ * The first 32 hex characters of SHA-256(`latch-run:<runId>`), the same id
7
+ * `@intentface/latch-langfuse` produced, so traces recorded before keep
8
+ * matching.
9
+ */
10
+ export declare function traceIdForRun(runId: string): Promise<string>;
11
+ //# 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":"AAAA;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKlE"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The trace id a latch run is recorded under: derived from the run id, so
3
+ * anything that knows the run (a feedback button, a later score writer, a link
4
+ * from your own UI) can address its trace without looking it up.
5
+ *
6
+ * The first 32 hex characters of SHA-256(`latch-run:<runId>`), the same id
7
+ * `@intentface/latch-langfuse` produced, so traces recorded before keep
8
+ * matching.
9
+ */
10
+ export async function traceIdForRun(runId) {
11
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`latch-run:${runId}`));
12
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0"))
13
+ .join("")
14
+ .slice(0, 32);
15
+ }
16
+ //# sourceMappingURL=trace-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace-id.js","sourceRoot":"","sources":["../src/trace-id.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAa;IAC/C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,aAAa,KAAK,EAAE,CAAC,CAAC,CAAC;IACrG,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SAC9E,IAAI,CAAC,EAAE,CAAC;SACR,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@intentface/latch-otel",
3
+ "version": "0.22.0",
4
+ "description": "Latch tracing over OpenTelemetry — one trace per run, the AI SDK's GenAI spans under it, sent over OTLP to any backend, with a ready preset for Langfuse.",
5
+ "keywords": [
6
+ "ai",
7
+ "ai-sdk",
8
+ "agents",
9
+ "llm",
10
+ "opentelemetry",
11
+ "otlp",
12
+ "observability",
13
+ "langfuse"
14
+ ],
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ },
25
+ "./langfuse": {
26
+ "types": "./dist/langfuse.d.ts",
27
+ "import": "./dist/langfuse.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "sideEffects": false,
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "registry": "https://registry.npmjs.org"
37
+ },
38
+ "dependencies": {
39
+ "@opentelemetry/api": "^1.9.0",
40
+ "@opentelemetry/context-async-hooks": "^2.7.1",
41
+ "@opentelemetry/exporter-trace-otlp-http": "^0.217.0",
42
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
43
+ "@intentface/latch-core": "^0.22.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@ai-sdk/otel": ">=1.0.0",
47
+ "ai": ">=7.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "@ai-sdk/otel": "1.0.106",
51
+ "@libsql/client": "^0.15.0",
52
+ "@types/node": "^22",
53
+ "ai": "^7.0.106",
54
+ "drizzle-orm": "^0.45.0",
55
+ "typescript": "^5.7.0",
56
+ "vitest": "^4",
57
+ "zod": "^4.0.0",
58
+ "@intentface/latch-drizzle": "^0.22.0"
59
+ },
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/Intentface/intentface-latch.git",
63
+ "directory": "packages/otel"
64
+ },
65
+ "scripts": {
66
+ "build": "tsc -p tsconfig.build.json",
67
+ "typecheck": "tsc -p tsconfig.json --noEmit",
68
+ "test": "vitest run"
69
+ }
70
+ }