langsmith 0.5.9 → 0.5.11

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,350 @@
1
+ "use strict";
2
+ /**
3
+ * WebSocket-based command execution for long-running commands.
4
+ *
5
+ * Uses the `ws` npm package (optional peer dependency).
6
+ * Install with: npm install ws
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.WSStreamControl = void 0;
10
+ exports.buildWsUrl = buildWsUrl;
11
+ exports.buildAuthHeaders = buildAuthHeaders;
12
+ exports.raiseForWsError = raiseForWsError;
13
+ exports.runWsStream = runWsStream;
14
+ exports.reconnectWsStream = reconnectWsStream;
15
+ const errors_js_1 = require("./errors.cjs");
16
+ async function ensureWs() {
17
+ try {
18
+ const ws = await import("ws");
19
+ return { WebSocket: ws.default || ws.WebSocket || ws };
20
+ }
21
+ catch {
22
+ throw new Error("WebSocket-based execution requires the 'ws' package. " +
23
+ "Install it with: npm install ws");
24
+ }
25
+ }
26
+ // =============================================================================
27
+ // URL and Auth Helpers
28
+ // =============================================================================
29
+ /**
30
+ * Convert a dataplane HTTP URL to a WebSocket URL for /execute/ws.
31
+ */
32
+ function buildWsUrl(dataplaneUrl) {
33
+ const wsUrl = dataplaneUrl
34
+ .replace("https://", "wss://")
35
+ .replace("http://", "ws://");
36
+ return `${wsUrl}/execute/ws`;
37
+ }
38
+ /**
39
+ * Build auth headers for the WebSocket upgrade request.
40
+ */
41
+ function buildAuthHeaders(apiKey) {
42
+ if (apiKey) {
43
+ return { "X-Api-Key": apiKey };
44
+ }
45
+ return {};
46
+ }
47
+ // =============================================================================
48
+ // Stream Control
49
+ // =============================================================================
50
+ /**
51
+ * Control interface for an active WebSocket stream.
52
+ *
53
+ * Created before the async generator starts, bound to the WebSocket once
54
+ * the connection opens. The CommandHandle holds a reference to this
55
+ * object to send kill/input messages.
56
+ */
57
+ class WSStreamControl {
58
+ constructor() {
59
+ Object.defineProperty(this, "_ws", {
60
+ enumerable: true,
61
+ configurable: true,
62
+ writable: true,
63
+ value: null
64
+ });
65
+ Object.defineProperty(this, "_closed", {
66
+ enumerable: true,
67
+ configurable: true,
68
+ writable: true,
69
+ value: false
70
+ });
71
+ Object.defineProperty(this, "_killed", {
72
+ enumerable: true,
73
+ configurable: true,
74
+ writable: true,
75
+ value: false
76
+ });
77
+ }
78
+ /** Bind to the active WebSocket. Called inside the generator. */
79
+ _bind(ws) {
80
+ this._ws = ws;
81
+ }
82
+ /** Mark as closed. Called when the generator exits. */
83
+ _unbind() {
84
+ this._closed = true;
85
+ this._ws = null;
86
+ }
87
+ /** True if kill() has been called on this stream. */
88
+ get killed() {
89
+ return this._killed;
90
+ }
91
+ /** Send a kill message to abort the running command. */
92
+ sendKill() {
93
+ this._killed = true;
94
+ if (this._ws && !this._closed && this._ws.readyState === 1) {
95
+ this._ws.send(JSON.stringify({ type: "kill" }));
96
+ }
97
+ }
98
+ /** Send stdin data to the running command. */
99
+ sendInput(data) {
100
+ if (this._ws && !this._closed && this._ws.readyState === 1) {
101
+ this._ws.send(JSON.stringify({ type: "input", data }));
102
+ }
103
+ }
104
+ }
105
+ exports.WSStreamControl = WSStreamControl;
106
+ // =============================================================================
107
+ // Error Handling
108
+ // =============================================================================
109
+ /**
110
+ * Raise the appropriate exception from a server error message.
111
+ */
112
+ function raiseForWsError(msg, commandId = "") {
113
+ const errorType = msg.error_type ?? "CommandError";
114
+ const errorMsg = msg.error ?? "Unknown error";
115
+ if (errorType === "CommandTimeout") {
116
+ throw new errors_js_1.LangSmithCommandTimeoutError(errorMsg);
117
+ }
118
+ if (errorType === "CommandNotFound") {
119
+ throw new errors_js_1.LangSmithSandboxOperationError(commandId ? `Command not found: ${commandId}` : errorMsg, commandId ? "reconnect" : "command", errorType);
120
+ }
121
+ if (errorType === "SessionExpired") {
122
+ throw new errors_js_1.LangSmithSandboxOperationError(commandId ? `Session expired: ${commandId}` : errorMsg, commandId ? "reconnect" : "command", errorType);
123
+ }
124
+ throw new errors_js_1.LangSmithSandboxOperationError(errorMsg, commandId ? "reconnect" : "command", errorType);
125
+ }
126
+ // =============================================================================
127
+ // WebSocket Stream Helpers
128
+ // =============================================================================
129
+ /**
130
+ * Create a ws WebSocket connection and return a promise that resolves when open
131
+ * or rejects on error.
132
+ */
133
+ async function connectWs(url, headers) {
134
+ const { WebSocket: WS } = await ensureWs();
135
+ return new Promise((resolve, reject) => {
136
+ const ws = new WS(url, { headers });
137
+ ws.on("open", () => {
138
+ ws.removeAllListeners("error");
139
+ resolve(ws);
140
+ });
141
+ ws.on("error", (err) => {
142
+ ws.removeAllListeners("open");
143
+ reject(new errors_js_1.LangSmithSandboxConnectionError(`Failed to connect to sandbox WebSocket: ${err.message}`));
144
+ });
145
+ });
146
+ }
147
+ /**
148
+ * Read messages from a ws WebSocket as an async iterable.
149
+ *
150
+ * Yields parsed WsMessage objects. Handles close events and errors,
151
+ * mapping them to appropriate exceptions.
152
+ */
153
+ async function* readWsMessages(ws) {
154
+ // Buffer incoming messages so the consumer can process them at its own pace
155
+ const messageQueue = [];
156
+ let resolve = null;
157
+ let error = null;
158
+ let done = false;
159
+ const onMessage = (data) => {
160
+ const raw = typeof data === "string" ? data : data.toString();
161
+ const msg = JSON.parse(raw);
162
+ messageQueue.push(msg);
163
+ if (resolve) {
164
+ const r = resolve;
165
+ resolve = null;
166
+ r();
167
+ }
168
+ };
169
+ const onClose = (code, reason) => {
170
+ done = true;
171
+ if (code === 1001) {
172
+ error = new errors_js_1.LangSmithSandboxServerReloadError("Server is reloading, reconnect to resume");
173
+ }
174
+ else if (code !== 1000) {
175
+ error = new errors_js_1.LangSmithSandboxConnectionError(`WebSocket connection closed unexpectedly (code: ${code}, reason: ${reason.toString()})`);
176
+ }
177
+ if (resolve) {
178
+ const r = resolve;
179
+ resolve = null;
180
+ r();
181
+ }
182
+ };
183
+ const onError = (err) => {
184
+ done = true;
185
+ if (!error) {
186
+ error = new errors_js_1.LangSmithSandboxConnectionError(`WebSocket connection error: ${err.message}`);
187
+ }
188
+ if (resolve) {
189
+ const r = resolve;
190
+ resolve = null;
191
+ r();
192
+ }
193
+ };
194
+ ws.on("message", onMessage);
195
+ ws.on("close", onClose);
196
+ ws.on("error", onError);
197
+ try {
198
+ while (true) {
199
+ // Drain buffered messages first
200
+ while (messageQueue.length > 0) {
201
+ yield messageQueue.shift();
202
+ }
203
+ // If done and queue is empty, we're finished
204
+ if (done) {
205
+ if (error) {
206
+ throw error;
207
+ }
208
+ return;
209
+ }
210
+ // Wait for next message or close/error
211
+ await new Promise((r) => {
212
+ resolve = r;
213
+ });
214
+ }
215
+ }
216
+ finally {
217
+ ws.removeListener("message", onMessage);
218
+ ws.removeListener("close", onClose);
219
+ ws.removeListener("error", onError);
220
+ }
221
+ }
222
+ // =============================================================================
223
+ // Async Stream Functions
224
+ // =============================================================================
225
+ /**
226
+ * Execute a command over WebSocket, yielding raw message dicts.
227
+ *
228
+ * Returns a tuple of [async_message_iterator, control]. The control object
229
+ * provides sendKill() and sendInput() methods for the CommandHandle.
230
+ *
231
+ * The iterator yields WsMessage objects with a "type" field:
232
+ * - { type: "started", command_id: "...", pid: N }
233
+ * - { type: "stdout", data: "...", offset: N }
234
+ * - { type: "stderr", data: "...", offset: N }
235
+ * - { type: "exit", exit_code: N }
236
+ *
237
+ * If onStdout/onStderr callbacks are provided, they are invoked as
238
+ * data arrives in addition to yielding the messages.
239
+ */
240
+ async function runWsStream(dataplaneUrl, apiKey, command, options = {}) {
241
+ const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout = 300, killOnDisconnect = false, ttlSeconds = 600, pty, } = options;
242
+ const wsUrl = buildWsUrl(dataplaneUrl);
243
+ const headers = buildAuthHeaders(apiKey);
244
+ const control = new WSStreamControl();
245
+ async function* stream() {
246
+ let ws;
247
+ try {
248
+ ws = await connectWs(wsUrl, headers);
249
+ control._bind(ws);
250
+ // Send execute request
251
+ const payload = {
252
+ type: "execute",
253
+ command,
254
+ timeout_seconds: timeout,
255
+ shell,
256
+ idle_timeout_seconds: idleTimeout,
257
+ kill_on_disconnect: killOnDisconnect,
258
+ ttl_seconds: ttlSeconds,
259
+ };
260
+ if (env)
261
+ payload.env = env;
262
+ if (cwd)
263
+ payload.cwd = cwd;
264
+ if (commandId)
265
+ payload.command_id = commandId;
266
+ if (pty)
267
+ payload.pty = true;
268
+ ws.send(JSON.stringify(payload));
269
+ // Read messages until exit or error
270
+ for await (const msg of readWsMessages(ws)) {
271
+ const msgType = msg.type;
272
+ if (msgType === "started") {
273
+ yield msg;
274
+ }
275
+ else if (msgType === "stdout") {
276
+ if (onStdout)
277
+ onStdout(msg.data);
278
+ yield msg;
279
+ }
280
+ else if (msgType === "stderr") {
281
+ if (onStderr)
282
+ onStderr(msg.data);
283
+ yield msg;
284
+ }
285
+ else if (msgType === "exit") {
286
+ yield msg;
287
+ return;
288
+ }
289
+ else if (msgType === "error") {
290
+ raiseForWsError(msg);
291
+ }
292
+ }
293
+ }
294
+ finally {
295
+ control._unbind();
296
+ if (ws && ws.readyState === 1) {
297
+ ws.close();
298
+ }
299
+ }
300
+ }
301
+ return [stream(), control];
302
+ }
303
+ /**
304
+ * Reconnect to an existing command over WebSocket.
305
+ *
306
+ * Returns a tuple of [async_message_iterator, control], same as runWsStream.
307
+ * The iterator yields stdout, stderr, exit, and error messages.
308
+ * No 'started' message is sent on reconnection.
309
+ */
310
+ async function reconnectWsStream(dataplaneUrl, apiKey, commandId, options = {}) {
311
+ const { stdoutOffset = 0, stderrOffset = 0 } = options;
312
+ const wsUrl = buildWsUrl(dataplaneUrl);
313
+ const headers = buildAuthHeaders(apiKey);
314
+ const control = new WSStreamControl();
315
+ async function* stream() {
316
+ let ws;
317
+ try {
318
+ ws = await connectWs(wsUrl, headers);
319
+ control._bind(ws);
320
+ // Send reconnect request
321
+ ws.send(JSON.stringify({
322
+ type: "reconnect",
323
+ command_id: commandId,
324
+ stdout_offset: stdoutOffset,
325
+ stderr_offset: stderrOffset,
326
+ }));
327
+ // Read messages until exit or error
328
+ for await (const msg of readWsMessages(ws)) {
329
+ const msgType = msg.type;
330
+ if (msgType === "stdout" || msgType === "stderr") {
331
+ yield msg;
332
+ }
333
+ else if (msgType === "exit") {
334
+ yield msg;
335
+ return;
336
+ }
337
+ else if (msgType === "error") {
338
+ raiseForWsError(msg, commandId);
339
+ }
340
+ }
341
+ }
342
+ finally {
343
+ control._unbind();
344
+ if (ws && ws.readyState === 1) {
345
+ ws.close();
346
+ }
347
+ }
348
+ }
349
+ return [stream(), control];
350
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * WebSocket-based command execution for long-running commands.
3
+ *
4
+ * Uses the `ws` npm package (optional peer dependency).
5
+ * Install with: npm install ws
6
+ */
7
+ import type { WsMessage, WsRunOptions } from "./types.js";
8
+ type WsWebSocket = any;
9
+ /**
10
+ * Convert a dataplane HTTP URL to a WebSocket URL for /execute/ws.
11
+ */
12
+ export declare function buildWsUrl(dataplaneUrl: string): string;
13
+ /**
14
+ * Build auth headers for the WebSocket upgrade request.
15
+ */
16
+ export declare function buildAuthHeaders(apiKey: string | undefined): Record<string, string>;
17
+ /**
18
+ * Control interface for an active WebSocket stream.
19
+ *
20
+ * Created before the async generator starts, bound to the WebSocket once
21
+ * the connection opens. The CommandHandle holds a reference to this
22
+ * object to send kill/input messages.
23
+ */
24
+ export declare class WSStreamControl {
25
+ private _ws;
26
+ private _closed;
27
+ private _killed;
28
+ /** Bind to the active WebSocket. Called inside the generator. */
29
+ _bind(ws: WsWebSocket): void;
30
+ /** Mark as closed. Called when the generator exits. */
31
+ _unbind(): void;
32
+ /** True if kill() has been called on this stream. */
33
+ get killed(): boolean;
34
+ /** Send a kill message to abort the running command. */
35
+ sendKill(): void;
36
+ /** Send stdin data to the running command. */
37
+ sendInput(data: string): void;
38
+ }
39
+ /**
40
+ * Raise the appropriate exception from a server error message.
41
+ */
42
+ export declare function raiseForWsError(msg: WsMessage, commandId?: string): never;
43
+ /**
44
+ * Execute a command over WebSocket, yielding raw message dicts.
45
+ *
46
+ * Returns a tuple of [async_message_iterator, control]. The control object
47
+ * provides sendKill() and sendInput() methods for the CommandHandle.
48
+ *
49
+ * The iterator yields WsMessage objects with a "type" field:
50
+ * - { type: "started", command_id: "...", pid: N }
51
+ * - { type: "stdout", data: "...", offset: N }
52
+ * - { type: "stderr", data: "...", offset: N }
53
+ * - { type: "exit", exit_code: N }
54
+ *
55
+ * If onStdout/onStderr callbacks are provided, they are invoked as
56
+ * data arrives in addition to yielding the messages.
57
+ */
58
+ export declare function runWsStream(dataplaneUrl: string, apiKey: string | undefined, command: string, options?: WsRunOptions): Promise<[AsyncIterableIterator<WsMessage>, WSStreamControl]>;
59
+ /**
60
+ * Reconnect to an existing command over WebSocket.
61
+ *
62
+ * Returns a tuple of [async_message_iterator, control], same as runWsStream.
63
+ * The iterator yields stdout, stderr, exit, and error messages.
64
+ * No 'started' message is sent on reconnection.
65
+ */
66
+ export declare function reconnectWsStream(dataplaneUrl: string, apiKey: string | undefined, commandId: string, options?: {
67
+ stdoutOffset?: number;
68
+ stderrOffset?: number;
69
+ }): Promise<[AsyncIterableIterator<WsMessage>, WSStreamControl]>;
70
+ export {};