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.
- package/dist/address.cjs +119 -0
- package/dist/address.d.ts +13 -0
- package/dist/address.js +107 -0
- package/dist/client.cjs +58 -22
- package/dist/client.d.ts +12 -0
- package/dist/client.js +58 -22
- package/dist/experimental/otel/constants.cjs +5 -1
- package/dist/experimental/otel/constants.d.ts +3 -0
- package/dist/experimental/otel/constants.js +3 -0
- package/dist/experimental/otel/translator.cjs +14 -0
- package/dist/experimental/otel/translator.js +14 -0
- package/dist/index.cjs +4 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/run_trees.cjs +154 -24
- package/dist/run_trees.d.ts +13 -1
- package/dist/run_trees.js +154 -24
- package/dist/sandbox/errors.cjs +12 -1
- package/dist/sandbox/errors.d.ts +7 -0
- package/dist/sandbox/errors.js +10 -0
- package/dist/sandbox/index.cjs +6 -1
- package/dist/sandbox/index.d.ts +3 -1
- package/dist/sandbox/index.js +2 -1
- package/dist/sandbox/verify.cjs +300 -0
- package/dist/sandbox/verify.d.ts +122 -0
- package/dist/sandbox/verify.js +296 -0
- package/dist/schemas.d.ts +5 -0
- package/dist/traceable.cjs +7 -1
- package/dist/traceable.js +7 -1
- package/dist/utils/agent_addressing.cjs +92 -0
- package/dist/utils/agent_addressing.d.ts +30 -0
- package/dist/utils/agent_addressing.js +83 -0
- package/dist/utils/env.cjs +2 -0
- package/dist/utils/env.js +2 -0
- package/package.json +5 -5
|
@@ -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
|
/**
|
package/dist/traceable.cjs
CHANGED
|
@@ -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
|
+
}
|
package/dist/utils/env.cjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
494
|
-
"undici@>=7.0.0 <8.0.0": "7.29.
|
|
495
|
-
"undici@>=8.0.0 <9.0.0": "8.
|
|
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",
|