langsmith 0.10.5 → 0.10.6

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.
@@ -0,0 +1,296 @@
1
+ import { getLangSmithEnvironmentVariable } from "../utils/env.js";
2
+ import { LangSmithSandboxTokenVerificationError } from "./errors.js";
3
+ /** Header carrying the signed identity of a LangSmith-login service URL request. */
4
+ export const USER_TOKEN_HEADER = "X-Langsmith-User-Token";
5
+ /** Header carrying the signature of a proxy callback request. */
6
+ export const CALLBACK_SIGNATURE_HEADER = "X-LangSmith-Signature-JWT";
7
+ const CALLBACK_SUBJECT = "langsmith-sandbox-callback";
8
+ const JWKS_PATH = "/.well-known/jwks.json";
9
+ const JWKS_TTL_MS = 300_000;
10
+ // Bounds refetches when a token names a kid the cached set lacks.
11
+ const JWKS_MIN_REFRESH_MS = 30_000;
12
+ const LEEWAY_SECONDS = 30;
13
+ function fail(message) {
14
+ throw new LangSmithSandboxTokenVerificationError(message);
15
+ }
16
+ function subtle() {
17
+ const s = globalThis.crypto?.subtle;
18
+ if (!s)
19
+ fail("Web Crypto is not available in this runtime");
20
+ return s;
21
+ }
22
+ function base64UrlDecode(value) {
23
+ const b64 = value.replace(/-/g, "+").replace(/_/g, "/");
24
+ const bin = atob(b64 + "=".repeat((4 - (b64.length % 4)) % 4));
25
+ const out = new Uint8Array(bin.length);
26
+ for (let i = 0; i < bin.length; i++)
27
+ out[i] = bin.charCodeAt(i);
28
+ return out;
29
+ }
30
+ function decodeJson(segment) {
31
+ const value = JSON.parse(new TextDecoder().decode(base64UrlDecode(segment)));
32
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
33
+ throw new Error("not a JSON object");
34
+ }
35
+ return value;
36
+ }
37
+ function isLoopback(hostname) {
38
+ return (hostname === "localhost" ||
39
+ hostname === "[::1]" ||
40
+ /^127(\.\d{1,3}){3}$/.test(hostname));
41
+ }
42
+ function checkJwksUrl(url, allowInsecure) {
43
+ const { protocol, hostname } = new URL(url);
44
+ if (protocol === "https:" || allowInsecure)
45
+ return;
46
+ if (protocol === "http:" && isLoopback(hostname))
47
+ return;
48
+ throw new Error(`JWKS URL must use https, got ${JSON.stringify(url)}; pass ` +
49
+ "allowInsecureJwks: true only if the network path to LangSmith is trusted");
50
+ }
51
+ function jwksUrlFrom(apiUrl) {
52
+ return new URL(JWKS_PATH, new URL(apiUrl).origin).toString();
53
+ }
54
+ function serviceHost(audience) {
55
+ return audience.includes("://") ? new URL(audience).host : audience;
56
+ }
57
+ function toBytes(body) {
58
+ if (typeof body === "string")
59
+ return new TextEncoder().encode(body);
60
+ if (body instanceof ArrayBuffer)
61
+ return new Uint8Array(body);
62
+ return new Uint8Array(body);
63
+ }
64
+ function hex(buf) {
65
+ return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
66
+ }
67
+ function timingSafeEqual(a, b) {
68
+ if (a.length !== b.length)
69
+ return false;
70
+ let diff = 0;
71
+ for (let i = 0; i < a.length; i++)
72
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
73
+ return diff === 0;
74
+ }
75
+ /**
76
+ * Verifies tokens LangSmith signs for code running in or behind a sandbox.
77
+ *
78
+ * Keys are fetched from LangSmith's JWKS endpoint and cached. Requires Web
79
+ * Crypto Ed25519 support (Node.js 20+, Deno, Bun, Cloudflare Workers).
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * const verifier = new SandboxTokenVerifier();
84
+ *
85
+ * // In an app served from a LangSmith-login service URL:
86
+ * const user = await verifier.verifyUserToken(
87
+ * req.headers.get(USER_TOKEN_HEADER)!,
88
+ * { audience: req.headers.get("host")! }
89
+ * );
90
+ *
91
+ * // In a proxy callback endpoint:
92
+ * const callback = await verifier.verifyCallback({
93
+ * body: await req.text(),
94
+ * signature: req.headers.get(CALLBACK_SIGNATURE_HEADER)!,
95
+ * aud: "https://example.com/sandbox-callback",
96
+ * });
97
+ * ```
98
+ */
99
+ export class SandboxTokenVerifier {
100
+ constructor(config = {}) {
101
+ Object.defineProperty(this, "jwksUrl", {
102
+ enumerable: true,
103
+ configurable: true,
104
+ writable: true,
105
+ value: void 0
106
+ });
107
+ Object.defineProperty(this, "timeoutMs", {
108
+ enumerable: true,
109
+ configurable: true,
110
+ writable: true,
111
+ value: void 0
112
+ });
113
+ Object.defineProperty(this, "keys", {
114
+ enumerable: true,
115
+ configurable: true,
116
+ writable: true,
117
+ value: new Map()
118
+ });
119
+ Object.defineProperty(this, "fetchedAt", {
120
+ enumerable: true,
121
+ configurable: true,
122
+ writable: true,
123
+ value: 0
124
+ });
125
+ Object.defineProperty(this, "inflight", {
126
+ enumerable: true,
127
+ configurable: true,
128
+ writable: true,
129
+ value: void 0
130
+ });
131
+ this.jwksUrl =
132
+ config.jwksUrl ??
133
+ jwksUrlFrom(config.apiUrl ??
134
+ getLangSmithEnvironmentVariable("ENDPOINT") ??
135
+ "https://api.smith.langchain.com");
136
+ checkJwksUrl(this.jwksUrl, config.allowInsecureJwks ?? false);
137
+ this.timeoutMs = config.timeoutMs ?? 10_000;
138
+ }
139
+ /**
140
+ * Verify the `X-Langsmith-User-Token` header of a service URL request.
141
+ *
142
+ * LangSmith sets this header only for service URLs that use LangSmith login.
143
+ * Code that knows it is running in a sandbox can instead trust the unsigned
144
+ * `X-Langsmith-User-Id` and `X-Langsmith-User-Email` headers, which the
145
+ * sandbox runtime strips from inbound requests and sets itself; verify this
146
+ * token when that is not guaranteed.
147
+ */
148
+ async verifyUserToken(token, options) {
149
+ const claims = await this.verify(token, serviceHost(options.audience), options.issuer);
150
+ const sub = claims.sub;
151
+ if (typeof sub !== "string" || !sub || sub === CALLBACK_SUBJECT) {
152
+ fail("token is not a user token");
153
+ }
154
+ return {
155
+ subject: sub,
156
+ email: typeof claims.email === "string" && claims.email
157
+ ? claims.email
158
+ : undefined,
159
+ name: typeof claims.name === "string" && claims.name
160
+ ? claims.name
161
+ : undefined,
162
+ expiresAt: new Date(claims.exp * 1000),
163
+ };
164
+ }
165
+ /** Verify a proxy callback request and return its parsed payload. */
166
+ async verifyCallback(options) {
167
+ const claims = await this.verify(options.signature, options.aud, options.issuer);
168
+ if (claims.sub !== CALLBACK_SUBJECT) {
169
+ fail("signature is not a callback signature");
170
+ }
171
+ const expected = claims.body_sha256;
172
+ if (typeof expected !== "string")
173
+ fail("signature has no body hash");
174
+ const raw = toBytes(options.body);
175
+ const digest = hex(await subtle().digest("SHA-256", raw));
176
+ if (!timingSafeEqual(expected, digest))
177
+ fail("body does not match signature");
178
+ let payload;
179
+ try {
180
+ payload = JSON.parse(new TextDecoder().decode(raw));
181
+ }
182
+ catch (e) {
183
+ fail(`malformed callback body: ${e.message}`);
184
+ }
185
+ const id = payload?.identity;
186
+ if (typeof payload?.host !== "string" ||
187
+ typeof payload.port !== "number" ||
188
+ typeof id?.tenant_id !== "string" ||
189
+ typeof id.sandbox_id !== "string") {
190
+ fail("malformed callback body");
191
+ }
192
+ return payload;
193
+ }
194
+ async verify(token, audience, issuer) {
195
+ const parts = token ? token.split(".") : [];
196
+ if (parts.length !== 3)
197
+ fail("malformed token");
198
+ let header;
199
+ let claims;
200
+ let signature;
201
+ try {
202
+ header = decodeJson(parts[0]);
203
+ claims = decodeJson(parts[1]);
204
+ signature = base64UrlDecode(parts[2]);
205
+ }
206
+ catch (e) {
207
+ fail(`malformed token: ${e.message}`);
208
+ }
209
+ if (header.alg !== "EdDSA") {
210
+ fail(`unexpected signing algorithm: ${JSON.stringify(header.alg)}`);
211
+ }
212
+ if (typeof header.kid !== "string" || !header.kid)
213
+ fail("token has no kid");
214
+ const key = await this.key(header.kid);
215
+ const signed = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
216
+ if (!(await subtle().verify({ name: "Ed25519" }, key, signature, signed))) {
217
+ fail("invalid signature");
218
+ }
219
+ const now = Date.now() / 1000;
220
+ const { exp, iat, nbf, iss, aud } = claims;
221
+ if (typeof exp !== "number")
222
+ fail("token has no exp");
223
+ if (now > exp + LEEWAY_SECONDS)
224
+ fail("token is expired");
225
+ if (typeof iat !== "number")
226
+ fail("token has no iat");
227
+ if (iat > now + LEEWAY_SECONDS)
228
+ fail("token is not yet valid");
229
+ if (nbf !== undefined &&
230
+ (typeof nbf !== "number" || nbf > now + LEEWAY_SECONDS)) {
231
+ fail("token is not yet valid");
232
+ }
233
+ if (typeof iss !== "string")
234
+ fail("token has no iss");
235
+ if (issuer !== undefined && iss !== issuer.replace(/\/+$/, "")) {
236
+ fail("token has the wrong issuer");
237
+ }
238
+ const audiences = typeof aud === "string" ? [aud] : Array.isArray(aud) ? aud : [];
239
+ const matches = typeof audience === "function"
240
+ ? audiences.some((a) => typeof a === "string" && audience(a))
241
+ : audiences.includes(audience);
242
+ if (audience !== undefined && !matches) {
243
+ fail("token has the wrong audience");
244
+ }
245
+ return claims;
246
+ }
247
+ async key(kid) {
248
+ const age = Date.now() - this.fetchedAt;
249
+ const fresh = this.keys.has(kid) && age < JWKS_TTL_MS;
250
+ if (!fresh && (this.fetchedAt === 0 || age >= JWKS_MIN_REFRESH_MS)) {
251
+ this.inflight ??= this.refresh().finally(() => {
252
+ this.inflight = undefined;
253
+ });
254
+ await this.inflight;
255
+ }
256
+ const key = this.keys.get(kid);
257
+ if (!key)
258
+ fail(`unknown signing key: ${kid}`);
259
+ try {
260
+ return await key;
261
+ }
262
+ catch (e) {
263
+ fail(`signing key ${kid} is not a usable Ed25519 key: ${e.message}`);
264
+ }
265
+ }
266
+ async refresh() {
267
+ let body;
268
+ try {
269
+ const resp = await fetch(this.jwksUrl, {
270
+ signal: AbortSignal.timeout(this.timeoutMs),
271
+ });
272
+ if (!resp.ok)
273
+ throw new Error(`HTTP ${resp.status}`);
274
+ body = await resp.json();
275
+ }
276
+ catch (e) {
277
+ fail(`failed to fetch JWKS from ${this.jwksUrl}: ${e.message}`);
278
+ }
279
+ const entries = body?.keys;
280
+ if (!Array.isArray(entries)) {
281
+ fail("invalid JWKS: expected an object with a keys array");
282
+ }
283
+ const keys = new Map();
284
+ for (const jwk of entries) {
285
+ if (typeof jwk !== "object" || jwk === null)
286
+ continue;
287
+ if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.kid || !jwk.x)
288
+ continue;
289
+ const imported = subtle().importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, { name: "Ed25519" }, false, ["verify"]);
290
+ imported.catch(() => undefined);
291
+ keys.set(jwk.kid, imported);
292
+ }
293
+ this.keys = keys;
294
+ this.fetchedAt = Date.now();
295
+ }
296
+ }
package/dist/schemas.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Address } from "./address.js";
1
2
  export interface TracerSession {
2
3
  tenant_id: string;
3
4
  id: string;
@@ -149,6 +150,8 @@ export interface RunCreate extends BaseRun {
149
150
  revision_id?: string;
150
151
  child_runs?: this[];
151
152
  session_name?: string;
153
+ /** (beta) Sent as `agent_id` / `agent_environment`. */
154
+ address?: Address;
152
155
  }
153
156
  export interface RunUpdate {
154
157
  id?: string;
@@ -167,6 +170,8 @@ export interface RunUpdate {
167
170
  events?: KVMap[];
168
171
  session_id?: string;
169
172
  session_name?: string;
173
+ /** (beta) Sent as `agent_id` / `agent_environment`. */
174
+ address?: Address;
170
175
  /** Unique ID assigned to every run within this nested trace. **/
171
176
  trace_id?: string;
172
177
  /**
@@ -9,6 +9,7 @@ const traceable_js_1 = require("./singletons/traceable.cjs");
9
9
  const constants_js_1 = require("./singletons/constants.cjs");
10
10
  const asserts_js_1 = require("./utils/asserts.cjs");
11
11
  const index_js_1 = require("./index.cjs");
12
+ const agent_addressing_js_1 = require("./utils/agent_addressing.cjs");
12
13
  const otel_js_1 = require("./singletons/otel.cjs");
13
14
  const utils_js_1 = require("./experimental/otel/utils.cjs");
14
15
  const constants_js_2 = require("./experimental/otel/constants.cjs");
@@ -472,13 +473,15 @@ const convertSerializableArg = (arg, options) => {
472
473
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
473
474
  function traceable(wrappedFunc, config) {
474
475
  const { aggregator, argsConfigPath, __finalTracedIteratorKey, processInputs, processOutputs, extractAttachments, on_start, ...runTreeConfig } = config ?? {};
476
+ runTreeConfig.address = (0, agent_addressing_js_1.checkAddress)(runTreeConfig.address);
477
+ (0, agent_addressing_js_1.rejectConflicting)(runTreeConfig.project_name, runTreeConfig.address);
475
478
  const processInputsFn = processInputs ?? ((x) => x);
476
479
  const processOutputsFn = processOutputs ?? ((x) => x);
477
480
  const extractAttachmentsFn = extractAttachments ?? ((...x) => [undefined, runInputsToMap(x)]);
478
481
  const traceableFunc = (...args) => {
479
482
  let ensuredConfig;
483
+ let runtimeConfig;
480
484
  try {
481
- let runtimeConfig;
482
485
  if (argsConfigPath) {
483
486
  const [index, path] = argsConfigPath;
484
487
  if (index === args.length - 1 && !path) {
@@ -520,7 +523,10 @@ function traceable(wrappedFunc, config) {
520
523
  name: wrappedFunc.name || "<lambda>",
521
524
  ...runTreeConfig,
522
525
  };
526
+ runtimeConfig = undefined;
523
527
  }
528
+ // Runtime config outranks decorator config, whichever mode each names.
529
+ [ensuredConfig.project_name, ensuredConfig.address] = (0, agent_addressing_js_1.firstNamed)([runtimeConfig?.project_name, runtimeConfig?.address], [runTreeConfig.project_name, runTreeConfig.address]);
524
530
  let runEndedPromiseResolver;
525
531
  const runEndedPromise = new Promise((resolve) => {
526
532
  runEndedPromiseResolver = resolve;
package/dist/traceable.js CHANGED
@@ -5,6 +5,7 @@ import { ROOT, AsyncLocalStorageProviderSingleton, getCurrentRunTree, } from "./
5
5
  import { _LC_CHILD_RUN_END_PROMISES_KEY, _LC_CONTEXT_VARIABLES_KEY, _PROCESSING_FAILED_KEY, _processingFailed, } from "./singletons/constants.js";
6
6
  import { isKVMap, isReadableStream, isAsyncIterable, isIteratorLike, isThenable, isGenerator, isPromiseMethod, } from "./utils/asserts.js";
7
7
  import { __version__ } from "./index.js";
8
+ import { checkAddress, firstNamed, rejectConflicting, } from "./utils/agent_addressing.js";
8
9
  import { getOTELTrace, getOTELContext } from "./singletons/otel.js";
9
10
  import { getUuidFromOtelSpanId } from "./experimental/otel/utils.js";
10
11
  import { LANGSMITH_REFERENCE_EXAMPLE_ID, LANGSMITH_SESSION_NAME, LANGSMITH_TRACEABLE, } from "./experimental/otel/constants.js";
@@ -468,13 +469,15 @@ const convertSerializableArg = (arg, options) => {
468
469
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
469
470
  export function traceable(wrappedFunc, config) {
470
471
  const { aggregator, argsConfigPath, __finalTracedIteratorKey, processInputs, processOutputs, extractAttachments, on_start, ...runTreeConfig } = config ?? {};
472
+ runTreeConfig.address = checkAddress(runTreeConfig.address);
473
+ rejectConflicting(runTreeConfig.project_name, runTreeConfig.address);
471
474
  const processInputsFn = processInputs ?? ((x) => x);
472
475
  const processOutputsFn = processOutputs ?? ((x) => x);
473
476
  const extractAttachmentsFn = extractAttachments ?? ((...x) => [undefined, runInputsToMap(x)]);
474
477
  const traceableFunc = (...args) => {
475
478
  let ensuredConfig;
479
+ let runtimeConfig;
476
480
  try {
477
- let runtimeConfig;
478
481
  if (argsConfigPath) {
479
482
  const [index, path] = argsConfigPath;
480
483
  if (index === args.length - 1 && !path) {
@@ -516,7 +519,10 @@ export function traceable(wrappedFunc, config) {
516
519
  name: wrappedFunc.name || "<lambda>",
517
520
  ...runTreeConfig,
518
521
  };
522
+ runtimeConfig = undefined;
519
523
  }
524
+ // Runtime config outranks decorator config, whichever mode each names.
525
+ [ensuredConfig.project_name, ensuredConfig.address] = firstNamed([runtimeConfig?.project_name, runtimeConfig?.address], [runTreeConfig.project_name, runTreeConfig.address]);
520
526
  let runEndedPromiseResolver;
521
527
  const runEndedPromise = new Promise((resolve) => {
522
528
  runEndedPromiseResolver = resolve;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkAddress = checkAddress;
4
+ exports.rejectConflicting = rejectConflicting;
5
+ exports.firstNamed = firstNamed;
6
+ exports.resolveFromEnv = resolveFromEnv;
7
+ exports.logUntraced = logUntraced;
8
+ exports.warnOnEnv = warnOnEnv;
9
+ exports.applyToPayload = applyToPayload;
10
+ const address_js_1 = require("../address.cjs");
11
+ const env_js_1 = require("./env.cjs");
12
+ const warn_js_1 = require("./warn.cjs");
13
+ /** Validate an address at an entry point; the SDK carries the frozen copy. */
14
+ function checkAddress(address) {
15
+ return address == null ? undefined : (0, address_js_1.normalizeAddress)(address);
16
+ }
17
+ function rejectConflicting(project, address) {
18
+ if (project && address) {
19
+ throw new Error(`A run is sent to a project (${JSON.stringify(project)}) or to an ` +
20
+ `address (${JSON.stringify(address)}), not both.`);
21
+ }
22
+ }
23
+ /** The highest level naming a project or an address, whichever it names. */
24
+ function firstNamed(...tiers) {
25
+ for (const [project, address] of tiers) {
26
+ rejectConflicting(project, address);
27
+ if (project || address) {
28
+ return [project || undefined, address];
29
+ }
30
+ }
31
+ return [undefined, undefined];
32
+ }
33
+ function getEnvProject() {
34
+ return ((0, env_js_1.getLangSmithEnvironmentVariable)("PROJECT") ||
35
+ (0, env_js_1.getEnvironmentVariable)("LANGCHAIN_SESSION") ||
36
+ undefined);
37
+ }
38
+ /**
39
+ * The env's destination: its address, else its project, else "default".
40
+ * @throws {EnvAddressError} For half an address, or an address and a project.
41
+ */
42
+ function resolveFromEnv() {
43
+ const project = getEnvProject();
44
+ const address = (0, address_js_1.addressFromEnv)();
45
+ if (project && address) {
46
+ throw new address_js_1.EnvAddressError("LANGSMITH_AGENT_* and a project are both set in the environment.");
47
+ }
48
+ return address ? [undefined, address] : [project ?? "default", undefined];
49
+ }
50
+ function logUntraced(error) {
51
+ (0, warn_js_1.warnOnce)(`LangSmith is not tracing this call: ${error.message}`);
52
+ }
53
+ /** Warn once, at client construction, if the env can't address runs. */
54
+ function warnOnEnv() {
55
+ if (!(0, address_js_1.envNames)().some((name) => (0, env_js_1.getEnvironmentVariable)(name))) {
56
+ return;
57
+ }
58
+ try {
59
+ resolveFromEnv();
60
+ }
61
+ catch (e) {
62
+ (0, warn_js_1.warnOnce)(`${e.message} Runs that name no destination in code are ` +
63
+ "not traced.");
64
+ }
65
+ }
66
+ /**
67
+ * Render `address` into its wire fields, in place. Without one, a create
68
+ * naming no project takes the env address. Already-rendered payloads (runs
69
+ * re-prepared when batched) are left alone.
70
+ *
71
+ * @throws {EnvAddressError} If the env names half an address.
72
+ */
73
+ function applyToPayload(run, { update = false } = {}) {
74
+ const payload = run;
75
+ let address = checkAddress(payload.address);
76
+ delete payload.address;
77
+ const namedProject = payload.session_id != null || payload.session_name != null;
78
+ const rendered = (0, address_js_1.wireKeys)().some((key) => payload[key] != null);
79
+ if (!address && !update && !namedProject && !rendered) {
80
+ address = (0, address_js_1.addressFromEnv)();
81
+ }
82
+ if (!address) {
83
+ return;
84
+ }
85
+ (0, warn_js_1.warnOnce)("Sending runs to an `address` is in beta and enabled per workspace; a " +
86
+ "workspace without it rejects the runs.");
87
+ Object.assign(payload, (0, address_js_1.toWire)(address));
88
+ if (!namedProject) {
89
+ delete payload.session_name;
90
+ delete payload.session_id;
91
+ }
92
+ }
@@ -0,0 +1,30 @@
1
+ import { type Address, EnvAddressError } from "../address.js";
2
+ /** One precedence level: the `[project, address]` it names. */
3
+ export type Tier = [string | undefined, Address | undefined];
4
+ /** Validate an address at an entry point; the SDK carries the frozen copy. */
5
+ export declare function checkAddress(address: unknown): Readonly<Address> | undefined;
6
+ export declare function rejectConflicting(project: unknown, address: unknown): void;
7
+ /** The highest level naming a project or an address, whichever it names. */
8
+ export declare function firstNamed(...tiers: Tier[]): Tier;
9
+ /**
10
+ * The env's destination: its address, else its project, else "default".
11
+ * @throws {EnvAddressError} For half an address, or an address and a project.
12
+ */
13
+ export declare function resolveFromEnv(): Tier;
14
+ export declare function logUntraced(error: EnvAddressError): void;
15
+ /** Warn once, at client construction, if the env can't address runs. */
16
+ export declare function warnOnEnv(): void;
17
+ /**
18
+ * Render `address` into its wire fields, in place. Without one, a create
19
+ * naming no project takes the env address. Already-rendered payloads (runs
20
+ * re-prepared when batched) are left alone.
21
+ *
22
+ * @throws {EnvAddressError} If the env names half an address.
23
+ */
24
+ export declare function applyToPayload(run: {
25
+ address?: Address;
26
+ session_id?: string;
27
+ session_name?: string;
28
+ }, { update }?: {
29
+ update?: boolean;
30
+ }): void;
@@ -0,0 +1,83 @@
1
+ import { EnvAddressError, addressFromEnv, envNames, normalizeAddress, toWire, wireKeys, } from "../address.js";
2
+ import { getEnvironmentVariable, getLangSmithEnvironmentVariable, } from "./env.js";
3
+ import { warnOnce } from "./warn.js";
4
+ /** Validate an address at an entry point; the SDK carries the frozen copy. */
5
+ export function checkAddress(address) {
6
+ return address == null ? undefined : normalizeAddress(address);
7
+ }
8
+ export function rejectConflicting(project, address) {
9
+ if (project && address) {
10
+ throw new Error(`A run is sent to a project (${JSON.stringify(project)}) or to an ` +
11
+ `address (${JSON.stringify(address)}), not both.`);
12
+ }
13
+ }
14
+ /** The highest level naming a project or an address, whichever it names. */
15
+ export function firstNamed(...tiers) {
16
+ for (const [project, address] of tiers) {
17
+ rejectConflicting(project, address);
18
+ if (project || address) {
19
+ return [project || undefined, address];
20
+ }
21
+ }
22
+ return [undefined, undefined];
23
+ }
24
+ function getEnvProject() {
25
+ return (getLangSmithEnvironmentVariable("PROJECT") ||
26
+ getEnvironmentVariable("LANGCHAIN_SESSION") ||
27
+ undefined);
28
+ }
29
+ /**
30
+ * The env's destination: its address, else its project, else "default".
31
+ * @throws {EnvAddressError} For half an address, or an address and a project.
32
+ */
33
+ export function resolveFromEnv() {
34
+ const project = getEnvProject();
35
+ const address = addressFromEnv();
36
+ if (project && address) {
37
+ throw new EnvAddressError("LANGSMITH_AGENT_* and a project are both set in the environment.");
38
+ }
39
+ return address ? [undefined, address] : [project ?? "default", undefined];
40
+ }
41
+ export function logUntraced(error) {
42
+ warnOnce(`LangSmith is not tracing this call: ${error.message}`);
43
+ }
44
+ /** Warn once, at client construction, if the env can't address runs. */
45
+ export function warnOnEnv() {
46
+ if (!envNames().some((name) => getEnvironmentVariable(name))) {
47
+ return;
48
+ }
49
+ try {
50
+ resolveFromEnv();
51
+ }
52
+ catch (e) {
53
+ warnOnce(`${e.message} Runs that name no destination in code are ` +
54
+ "not traced.");
55
+ }
56
+ }
57
+ /**
58
+ * Render `address` into its wire fields, in place. Without one, a create
59
+ * naming no project takes the env address. Already-rendered payloads (runs
60
+ * re-prepared when batched) are left alone.
61
+ *
62
+ * @throws {EnvAddressError} If the env names half an address.
63
+ */
64
+ export function applyToPayload(run, { update = false } = {}) {
65
+ const payload = run;
66
+ let address = checkAddress(payload.address);
67
+ delete payload.address;
68
+ const namedProject = payload.session_id != null || payload.session_name != null;
69
+ const rendered = wireKeys().some((key) => payload[key] != null);
70
+ if (!address && !update && !namedProject && !rendered) {
71
+ address = addressFromEnv();
72
+ }
73
+ if (!address) {
74
+ return;
75
+ }
76
+ warnOnce("Sending runs to an `address` is in beta and enabled per workspace; a " +
77
+ "workspace without it rejects the runs.");
78
+ Object.assign(payload, toWire(address));
79
+ if (!namedProject) {
80
+ delete payload.session_name;
81
+ delete payload.session_id;
82
+ }
83
+ }
@@ -113,6 +113,8 @@ function getLangSmithEnvVarsMetadata() {
113
113
  "LANGSMITH_CONFIG_FILE",
114
114
  "LANGSMITH_PROJECT",
115
115
  "LANGSMITH_SESSION",
116
+ "LANGSMITH_AGENT_ID",
117
+ "LANGSMITH_AGENT_ENVIRONMENT",
116
118
  ];
117
119
  for (const [key, value] of Object.entries(allEnvVars)) {
118
120
  if (typeof value === "string" &&
package/dist/utils/env.js CHANGED
@@ -95,6 +95,8 @@ export function getLangSmithEnvVarsMetadata() {
95
95
  "LANGSMITH_CONFIG_FILE",
96
96
  "LANGSMITH_PROJECT",
97
97
  "LANGSMITH_SESSION",
98
+ "LANGSMITH_AGENT_ID",
99
+ "LANGSMITH_AGENT_ENVIRONMENT",
98
100
  ];
99
101
  for (const [key, value] of Object.entries(allEnvVars)) {
100
102
  if (typeof value === "string" &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langsmith",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "description": "Client library to connect to the LangSmith Observability and Evaluation Platform.",
5
5
  "packageManager": "pnpm@10.33.0",
6
6
  "files": [
@@ -486,13 +486,13 @@
486
486
  "brace-expansion@<1.1.16": "1.1.16",
487
487
  "brace-expansion@>=2.0.0 <2.1.2": "2.1.2",
488
488
  "brace-expansion@>=3.0.0 <5.0.7": "5.0.7",
489
- "fast-uri": "3.1.6",
489
+ "fast-uri": "3.1.8",
490
490
  "postcss": "8.5.23",
491
491
  "js-yaml": "4.3.2",
492
492
  "hono": "4.13.5",
493
- "ip-address": "10.3.1",
494
- "undici@>=7.0.0 <8.0.0": "7.29.0",
495
- "undici@>=8.0.0 <9.0.0": "8.9.0",
493
+ "ip-address": "10.7.1",
494
+ "undici@>=7.0.0 <8.0.0": "7.29.1",
495
+ "undici@>=8.0.0 <9.0.0": "8.10.2",
496
496
  "vite": "^7.3.2",
497
497
  "form-data": "4.0.6",
498
498
  "langsmith": "0.6.0",