langsmith 0.10.4 → 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 +68 -22
- package/dist/client.d.ts +13 -0
- package/dist/client.js +68 -22
- package/dist/evaluation/_runner.cjs +47 -5
- package/dist/evaluation/_runner.d.ts +12 -0
- package/dist/evaluation/_runner.js +47 -5
- 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/access_delegation.cjs +24 -0
- package/dist/sandbox/access_delegation.d.ts +15 -0
- package/dist/sandbox/access_delegation.js +21 -0
- package/dist/sandbox/client.cjs +70 -5
- package/dist/sandbox/client.d.ts +18 -0
- package/dist/sandbox/client.js +70 -5
- package/dist/sandbox/command_handle.cjs +34 -0
- package/dist/sandbox/command_handle.d.ts +10 -0
- package/dist/sandbox/command_handle.js +34 -0
- 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/helpers.cjs +129 -0
- package/dist/sandbox/helpers.d.ts +29 -0
- package/dist/sandbox/helpers.js +123 -0
- package/dist/sandbox/index.cjs +12 -1
- package/dist/sandbox/index.d.ts +8 -2
- package/dist/sandbox/index.js +4 -1
- package/dist/sandbox/mounts.cjs +21 -3
- package/dist/sandbox/mounts.d.ts +7 -1
- package/dist/sandbox/mounts.js +21 -3
- package/dist/sandbox/proxy_config.cjs +41 -10
- package/dist/sandbox/proxy_config.d.ts +33 -6
- package/dist/sandbox/proxy_config.js +40 -10
- package/dist/sandbox/sandbox.cjs +160 -4
- package/dist/sandbox/sandbox.d.ts +71 -1
- package/dist/sandbox/sandbox.js +161 -5
- package/dist/sandbox/service_url.cjs +125 -0
- package/dist/sandbox/service_url.d.ts +50 -0
- package/dist/sandbox/service_url.js +120 -0
- package/dist/sandbox/types.d.ts +251 -8
- package/dist/sandbox/verify.cjs +300 -0
- package/dist/sandbox/verify.d.ts +122 -0
- package/dist/sandbox/verify.js +296 -0
- package/dist/sandbox/ws_execute.cjs +31 -1
- package/dist/sandbox/ws_execute.d.ts +9 -0
- package/dist/sandbox/ws_execute.js +31 -1
- 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/dist/utils/error.cjs +17 -22
- package/dist/utils/error.js +17 -22
- 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
|
+
}
|
|
@@ -136,10 +136,20 @@ class WSStreamControl {
|
|
|
136
136
|
writable: true,
|
|
137
137
|
value: false
|
|
138
138
|
});
|
|
139
|
+
Object.defineProperty(this, "_closeStdinPending", {
|
|
140
|
+
enumerable: true,
|
|
141
|
+
configurable: true,
|
|
142
|
+
writable: true,
|
|
143
|
+
value: false
|
|
144
|
+
});
|
|
139
145
|
}
|
|
140
146
|
/** Bind to the active WebSocket. Called inside the generator. */
|
|
141
147
|
_bind(ws) {
|
|
142
148
|
this._ws = ws;
|
|
149
|
+
if (this._closeStdinPending) {
|
|
150
|
+
this._closeStdinPending = false;
|
|
151
|
+
this.sendCloseStdin();
|
|
152
|
+
}
|
|
143
153
|
}
|
|
144
154
|
/** Mark as closed. Called when the generator exits. */
|
|
145
155
|
_unbind() {
|
|
@@ -163,6 +173,22 @@ class WSStreamControl {
|
|
|
163
173
|
this._ws.send(JSON.stringify({ type: "input", data }));
|
|
164
174
|
}
|
|
165
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Half-close the command's stdin so it reads EOF.
|
|
178
|
+
*
|
|
179
|
+
* A reconnected stream binds its socket only once iteration starts, so a
|
|
180
|
+
* close arriving before that is queued and sent on bind — dropping it would
|
|
181
|
+
* leave the command waiting for an EOF that never comes.
|
|
182
|
+
*/
|
|
183
|
+
sendCloseStdin() {
|
|
184
|
+
if (this._closed)
|
|
185
|
+
return;
|
|
186
|
+
if (this._ws && this._ws.readyState === 1) {
|
|
187
|
+
this._ws.send(JSON.stringify({ type: "close_stdin" }));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this._closeStdinPending = true;
|
|
191
|
+
}
|
|
166
192
|
}
|
|
167
193
|
exports.WSStreamControl = WSStreamControl;
|
|
168
194
|
// =============================================================================
|
|
@@ -330,7 +356,7 @@ async function* readWsMessages(ws) {
|
|
|
330
356
|
* data arrives in addition to yielding the messages.
|
|
331
357
|
*/
|
|
332
358
|
async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
|
|
333
|
-
const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, headers: extraHeaders, openTimeout = exports.WS_OPEN_TIMEOUT, } = options;
|
|
359
|
+
const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, runConfig, closeStdin, headers: extraHeaders, openTimeout = exports.WS_OPEN_TIMEOUT, } = options;
|
|
334
360
|
const wsUrl = buildWsUrl(dataplaneUrl);
|
|
335
361
|
const headers = buildAuthHeaders(apiKey, extraHeaders);
|
|
336
362
|
const control = new WSStreamControl();
|
|
@@ -353,6 +379,10 @@ async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
|
|
|
353
379
|
payload.env = env;
|
|
354
380
|
if (cwd)
|
|
355
381
|
payload.cwd = cwd;
|
|
382
|
+
if (runConfig)
|
|
383
|
+
payload.run_config = runConfig;
|
|
384
|
+
if (closeStdin)
|
|
385
|
+
payload.close_stdin = true;
|
|
356
386
|
if (commandId)
|
|
357
387
|
payload.command_id = commandId;
|
|
358
388
|
if (pty)
|
|
@@ -35,6 +35,7 @@ export declare class WSStreamControl {
|
|
|
35
35
|
private _ws;
|
|
36
36
|
private _closed;
|
|
37
37
|
private _killed;
|
|
38
|
+
private _closeStdinPending;
|
|
38
39
|
/** Bind to the active WebSocket. Called inside the generator. */
|
|
39
40
|
_bind(ws: WsWebSocket): void;
|
|
40
41
|
/** Mark as closed. Called when the generator exits. */
|
|
@@ -45,6 +46,14 @@ export declare class WSStreamControl {
|
|
|
45
46
|
sendKill(): void;
|
|
46
47
|
/** Send stdin data to the running command. */
|
|
47
48
|
sendInput(data: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Half-close the command's stdin so it reads EOF.
|
|
51
|
+
*
|
|
52
|
+
* A reconnected stream binds its socket only once iteration starts, so a
|
|
53
|
+
* close arriving before that is queued and sent on bind — dropping it would
|
|
54
|
+
* leave the command waiting for an EOF that never comes.
|
|
55
|
+
*/
|
|
56
|
+
sendCloseStdin(): void;
|
|
48
57
|
}
|
|
49
58
|
/**
|
|
50
59
|
* Raise the appropriate exception from a server error message.
|
|
@@ -124,10 +124,20 @@ export class WSStreamControl {
|
|
|
124
124
|
writable: true,
|
|
125
125
|
value: false
|
|
126
126
|
});
|
|
127
|
+
Object.defineProperty(this, "_closeStdinPending", {
|
|
128
|
+
enumerable: true,
|
|
129
|
+
configurable: true,
|
|
130
|
+
writable: true,
|
|
131
|
+
value: false
|
|
132
|
+
});
|
|
127
133
|
}
|
|
128
134
|
/** Bind to the active WebSocket. Called inside the generator. */
|
|
129
135
|
_bind(ws) {
|
|
130
136
|
this._ws = ws;
|
|
137
|
+
if (this._closeStdinPending) {
|
|
138
|
+
this._closeStdinPending = false;
|
|
139
|
+
this.sendCloseStdin();
|
|
140
|
+
}
|
|
131
141
|
}
|
|
132
142
|
/** Mark as closed. Called when the generator exits. */
|
|
133
143
|
_unbind() {
|
|
@@ -151,6 +161,22 @@ export class WSStreamControl {
|
|
|
151
161
|
this._ws.send(JSON.stringify({ type: "input", data }));
|
|
152
162
|
}
|
|
153
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Half-close the command's stdin so it reads EOF.
|
|
166
|
+
*
|
|
167
|
+
* A reconnected stream binds its socket only once iteration starts, so a
|
|
168
|
+
* close arriving before that is queued and sent on bind — dropping it would
|
|
169
|
+
* leave the command waiting for an EOF that never comes.
|
|
170
|
+
*/
|
|
171
|
+
sendCloseStdin() {
|
|
172
|
+
if (this._closed)
|
|
173
|
+
return;
|
|
174
|
+
if (this._ws && this._ws.readyState === 1) {
|
|
175
|
+
this._ws.send(JSON.stringify({ type: "close_stdin" }));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this._closeStdinPending = true;
|
|
179
|
+
}
|
|
154
180
|
}
|
|
155
181
|
// =============================================================================
|
|
156
182
|
// Error Handling
|
|
@@ -317,7 +343,7 @@ async function* readWsMessages(ws) {
|
|
|
317
343
|
* data arrives in addition to yielding the messages.
|
|
318
344
|
*/
|
|
319
345
|
export async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
|
|
320
|
-
const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, headers: extraHeaders, openTimeout = WS_OPEN_TIMEOUT, } = options;
|
|
346
|
+
const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, runConfig, closeStdin, headers: extraHeaders, openTimeout = WS_OPEN_TIMEOUT, } = options;
|
|
321
347
|
const wsUrl = buildWsUrl(dataplaneUrl);
|
|
322
348
|
const headers = buildAuthHeaders(apiKey, extraHeaders);
|
|
323
349
|
const control = new WSStreamControl();
|
|
@@ -340,6 +366,10 @@ export async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
|
|
|
340
366
|
payload.env = env;
|
|
341
367
|
if (cwd)
|
|
342
368
|
payload.cwd = cwd;
|
|
369
|
+
if (runConfig)
|
|
370
|
+
payload.run_config = runConfig;
|
|
371
|
+
if (closeStdin)
|
|
372
|
+
payload.close_stdin = true;
|
|
343
373
|
if (commandId)
|
|
344
374
|
payload.command_id = commandId;
|
|
345
375
|
if (pty)
|
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;
|