langsmith 0.5.9 → 0.5.10
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/client.cjs +33 -10
- package/dist/client.js +33 -10
- package/dist/experimental/sandbox/client.cjs +7 -0
- package/dist/experimental/sandbox/client.js +7 -0
- package/dist/experimental/sandbox/command_handle.cjs +325 -0
- package/dist/experimental/sandbox/command_handle.d.ts +98 -0
- package/dist/experimental/sandbox/command_handle.js +321 -0
- package/dist/experimental/sandbox/errors.cjs +30 -1
- package/dist/experimental/sandbox/errors.d.ts +15 -0
- package/dist/experimental/sandbox/errors.js +27 -0
- package/dist/experimental/sandbox/index.cjs +5 -1
- package/dist/experimental/sandbox/index.d.ts +3 -2
- package/dist/experimental/sandbox/index.js +3 -2
- package/dist/experimental/sandbox/sandbox.cjs +91 -28
- package/dist/experimental/sandbox/sandbox.d.ts +43 -19
- package/dist/experimental/sandbox/sandbox.js +91 -28
- package/dist/experimental/sandbox/types.d.ts +93 -0
- package/dist/experimental/sandbox/ws_execute.cjs +350 -0
- package/dist/experimental/sandbox/ws_execute.d.ts +70 -0
- package/dist/experimental/sandbox/ws_execute.js +341 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +9 -3
package/dist/client.cjs
CHANGED
|
@@ -50,6 +50,26 @@ const index_js_2 = require("./utils/prompt_cache/index.cjs");
|
|
|
50
50
|
const fsUtils = __importStar(require("./utils/fs.cjs"));
|
|
51
51
|
const fetch_js_1 = require("./singletons/fetch.cjs");
|
|
52
52
|
const index_js_3 = require("./utils/fast-safe-stringify/index.cjs");
|
|
53
|
+
/**
|
|
54
|
+
* Catches timestamps without a timezone suffix.
|
|
55
|
+
*/
|
|
56
|
+
function _ensureUTCTimestamp(ts) {
|
|
57
|
+
if (typeof ts === "string" &&
|
|
58
|
+
ts.length > 0 &&
|
|
59
|
+
!ts.includes("Z") &&
|
|
60
|
+
!ts.includes("+") &&
|
|
61
|
+
!ts.includes("-", 10)) {
|
|
62
|
+
return ts + "Z";
|
|
63
|
+
}
|
|
64
|
+
return ts;
|
|
65
|
+
}
|
|
66
|
+
function _normalizeRunTimestamps(run) {
|
|
67
|
+
return {
|
|
68
|
+
...run,
|
|
69
|
+
start_time: _ensureUTCTimestamp(run.start_time),
|
|
70
|
+
end_time: _ensureUTCTimestamp(run.end_time),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
53
73
|
function mergeRuntimeEnvIntoRun(run, cachedEnvVars, omitTracedRuntimeInfo) {
|
|
54
74
|
if (omitTracedRuntimeInfo) {
|
|
55
75
|
return run;
|
|
@@ -1542,7 +1562,7 @@ class Client {
|
|
|
1542
1562
|
}
|
|
1543
1563
|
async readRun(runId, { loadChildRuns } = { loadChildRuns: false }) {
|
|
1544
1564
|
(0, _uuid_js_1.assertUuid)(runId);
|
|
1545
|
-
let run = await this._get(`/runs/${runId}`);
|
|
1565
|
+
let run = _normalizeRunTimestamps(await this._get(`/runs/${runId}`));
|
|
1546
1566
|
if (loadChildRuns) {
|
|
1547
1567
|
run = await this._loadChildRuns(run);
|
|
1548
1568
|
}
|
|
@@ -1761,20 +1781,21 @@ class Client {
|
|
|
1761
1781
|
}
|
|
1762
1782
|
let runsYielded = 0;
|
|
1763
1783
|
for await (const runs of this._getCursorPaginatedList("/runs/query", body)) {
|
|
1784
|
+
const normalized = runs.map(_normalizeRunTimestamps);
|
|
1764
1785
|
if (limit) {
|
|
1765
1786
|
if (runsYielded >= limit) {
|
|
1766
1787
|
break;
|
|
1767
1788
|
}
|
|
1768
|
-
if (
|
|
1769
|
-
const newRuns =
|
|
1789
|
+
if (normalized.length + runsYielded > limit) {
|
|
1790
|
+
const newRuns = normalized.slice(0, limit - runsYielded);
|
|
1770
1791
|
yield* newRuns;
|
|
1771
1792
|
break;
|
|
1772
1793
|
}
|
|
1773
|
-
runsYielded +=
|
|
1774
|
-
yield*
|
|
1794
|
+
runsYielded += normalized.length;
|
|
1795
|
+
yield* normalized;
|
|
1775
1796
|
}
|
|
1776
1797
|
else {
|
|
1777
|
-
yield*
|
|
1798
|
+
yield* normalized;
|
|
1778
1799
|
}
|
|
1779
1800
|
}
|
|
1780
1801
|
}
|
|
@@ -1891,7 +1912,8 @@ class Client {
|
|
|
1891
1912
|
}
|
|
1892
1913
|
const threadsMap = new Map();
|
|
1893
1914
|
for await (const runs of this._getCursorPaginatedList("/runs/query", bodyQuery)) {
|
|
1894
|
-
for (const
|
|
1915
|
+
for (const raw of runs) {
|
|
1916
|
+
const run = _normalizeRunTimestamps(raw);
|
|
1895
1917
|
const tid = run.thread_id;
|
|
1896
1918
|
if (tid) {
|
|
1897
1919
|
const list = threadsMap.get(tid) ?? [];
|
|
@@ -2064,8 +2086,8 @@ class Client {
|
|
|
2064
2086
|
await (0, error_js_1.raiseForStatus)(res, "list shared runs");
|
|
2065
2087
|
return res;
|
|
2066
2088
|
});
|
|
2067
|
-
const runs = await response.json();
|
|
2068
|
-
return runs;
|
|
2089
|
+
const runs = (await response.json());
|
|
2090
|
+
return runs.map(_normalizeRunTimestamps);
|
|
2069
2091
|
}
|
|
2070
2092
|
async readDatasetSharedSchema(datasetId, datasetName) {
|
|
2071
2093
|
if (!datasetId && !datasetName) {
|
|
@@ -3714,7 +3736,8 @@ class Client {
|
|
|
3714
3736
|
await (0, error_js_1.raiseForStatus)(res, "get run from annotation queue");
|
|
3715
3737
|
return res;
|
|
3716
3738
|
});
|
|
3717
|
-
|
|
3739
|
+
const run = await response.json();
|
|
3740
|
+
return _normalizeRunTimestamps(run);
|
|
3718
3741
|
}
|
|
3719
3742
|
/**
|
|
3720
3743
|
* Delete a run from an an annotation queue.
|
package/dist/client.js
CHANGED
|
@@ -13,6 +13,26 @@ import { promptCacheSingleton, } from "./utils/prompt_cache/index.js";
|
|
|
13
13
|
import * as fsUtils from "./utils/fs.js";
|
|
14
14
|
import { _shouldStreamForGlobalFetchImplementation, _getFetchImplementation, } from "./singletons/fetch.js";
|
|
15
15
|
import { serialize as serializePayloadForTracing } from "./utils/fast-safe-stringify/index.js";
|
|
16
|
+
/**
|
|
17
|
+
* Catches timestamps without a timezone suffix.
|
|
18
|
+
*/
|
|
19
|
+
function _ensureUTCTimestamp(ts) {
|
|
20
|
+
if (typeof ts === "string" &&
|
|
21
|
+
ts.length > 0 &&
|
|
22
|
+
!ts.includes("Z") &&
|
|
23
|
+
!ts.includes("+") &&
|
|
24
|
+
!ts.includes("-", 10)) {
|
|
25
|
+
return ts + "Z";
|
|
26
|
+
}
|
|
27
|
+
return ts;
|
|
28
|
+
}
|
|
29
|
+
function _normalizeRunTimestamps(run) {
|
|
30
|
+
return {
|
|
31
|
+
...run,
|
|
32
|
+
start_time: _ensureUTCTimestamp(run.start_time),
|
|
33
|
+
end_time: _ensureUTCTimestamp(run.end_time),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
16
36
|
export function mergeRuntimeEnvIntoRun(run, cachedEnvVars, omitTracedRuntimeInfo) {
|
|
17
37
|
if (omitTracedRuntimeInfo) {
|
|
18
38
|
return run;
|
|
@@ -1504,7 +1524,7 @@ export class Client {
|
|
|
1504
1524
|
}
|
|
1505
1525
|
async readRun(runId, { loadChildRuns } = { loadChildRuns: false }) {
|
|
1506
1526
|
assertUuid(runId);
|
|
1507
|
-
let run = await this._get(`/runs/${runId}`);
|
|
1527
|
+
let run = _normalizeRunTimestamps(await this._get(`/runs/${runId}`));
|
|
1508
1528
|
if (loadChildRuns) {
|
|
1509
1529
|
run = await this._loadChildRuns(run);
|
|
1510
1530
|
}
|
|
@@ -1723,20 +1743,21 @@ export class Client {
|
|
|
1723
1743
|
}
|
|
1724
1744
|
let runsYielded = 0;
|
|
1725
1745
|
for await (const runs of this._getCursorPaginatedList("/runs/query", body)) {
|
|
1746
|
+
const normalized = runs.map(_normalizeRunTimestamps);
|
|
1726
1747
|
if (limit) {
|
|
1727
1748
|
if (runsYielded >= limit) {
|
|
1728
1749
|
break;
|
|
1729
1750
|
}
|
|
1730
|
-
if (
|
|
1731
|
-
const newRuns =
|
|
1751
|
+
if (normalized.length + runsYielded > limit) {
|
|
1752
|
+
const newRuns = normalized.slice(0, limit - runsYielded);
|
|
1732
1753
|
yield* newRuns;
|
|
1733
1754
|
break;
|
|
1734
1755
|
}
|
|
1735
|
-
runsYielded +=
|
|
1736
|
-
yield*
|
|
1756
|
+
runsYielded += normalized.length;
|
|
1757
|
+
yield* normalized;
|
|
1737
1758
|
}
|
|
1738
1759
|
else {
|
|
1739
|
-
yield*
|
|
1760
|
+
yield* normalized;
|
|
1740
1761
|
}
|
|
1741
1762
|
}
|
|
1742
1763
|
}
|
|
@@ -1853,7 +1874,8 @@ export class Client {
|
|
|
1853
1874
|
}
|
|
1854
1875
|
const threadsMap = new Map();
|
|
1855
1876
|
for await (const runs of this._getCursorPaginatedList("/runs/query", bodyQuery)) {
|
|
1856
|
-
for (const
|
|
1877
|
+
for (const raw of runs) {
|
|
1878
|
+
const run = _normalizeRunTimestamps(raw);
|
|
1857
1879
|
const tid = run.thread_id;
|
|
1858
1880
|
if (tid) {
|
|
1859
1881
|
const list = threadsMap.get(tid) ?? [];
|
|
@@ -2026,8 +2048,8 @@ export class Client {
|
|
|
2026
2048
|
await raiseForStatus(res, "list shared runs");
|
|
2027
2049
|
return res;
|
|
2028
2050
|
});
|
|
2029
|
-
const runs = await response.json();
|
|
2030
|
-
return runs;
|
|
2051
|
+
const runs = (await response.json());
|
|
2052
|
+
return runs.map(_normalizeRunTimestamps);
|
|
2031
2053
|
}
|
|
2032
2054
|
async readDatasetSharedSchema(datasetId, datasetName) {
|
|
2033
2055
|
if (!datasetId && !datasetName) {
|
|
@@ -3676,7 +3698,8 @@ export class Client {
|
|
|
3676
3698
|
await raiseForStatus(res, "get run from annotation queue");
|
|
3677
3699
|
return res;
|
|
3678
3700
|
});
|
|
3679
|
-
|
|
3701
|
+
const run = await response.json();
|
|
3702
|
+
return _normalizeRunTimestamps(run);
|
|
3680
3703
|
}
|
|
3681
3704
|
/**
|
|
3682
3705
|
* Delete a run from an an annotation queue.
|
|
@@ -106,6 +106,13 @@ class SandboxClient {
|
|
|
106
106
|
headers,
|
|
107
107
|
}));
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Get the API key for WebSocket authentication.
|
|
111
|
+
* @internal
|
|
112
|
+
*/
|
|
113
|
+
getApiKey() {
|
|
114
|
+
return this._apiKey;
|
|
115
|
+
}
|
|
109
116
|
// =========================================================================
|
|
110
117
|
// Volume Operations
|
|
111
118
|
// =========================================================================
|
|
@@ -103,6 +103,13 @@ export class SandboxClient {
|
|
|
103
103
|
headers,
|
|
104
104
|
}));
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Get the API key for WebSocket authentication.
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
getApiKey() {
|
|
111
|
+
return this._apiKey;
|
|
112
|
+
}
|
|
106
113
|
// =========================================================================
|
|
107
114
|
// Volume Operations
|
|
108
115
|
// =========================================================================
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CommandHandle - async handle to a running command with streaming output
|
|
4
|
+
* and auto-reconnect.
|
|
5
|
+
*
|
|
6
|
+
* Port of Python's AsyncCommandHandle to TypeScript.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.CommandHandle = void 0;
|
|
10
|
+
const errors_js_1 = require("./errors.cjs");
|
|
11
|
+
/**
|
|
12
|
+
* Async handle to a running command with streaming output and auto-reconnect.
|
|
13
|
+
*
|
|
14
|
+
* Async iterable, yielding OutputChunk objects (stdout and stderr interleaved
|
|
15
|
+
* in arrival order). Access .result after iteration to get the full
|
|
16
|
+
* ExecutionResult.
|
|
17
|
+
*
|
|
18
|
+
* Auto-reconnect behavior:
|
|
19
|
+
* - Server hot-reload (1001 Going Away): reconnect immediately
|
|
20
|
+
* - Network error / unexpected close: reconnect with exponential backoff
|
|
21
|
+
* - User called kill(): do NOT reconnect (propagate error)
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const handle = await sandbox.run("make build", { timeout: 600, wait: false });
|
|
26
|
+
*
|
|
27
|
+
* for await (const chunk of handle) { // auto-reconnects on transient errors
|
|
28
|
+
* process.stdout.write(chunk.data);
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* const result = await handle.result;
|
|
32
|
+
* console.log(`Exit code: ${result.exit_code}`);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
class CommandHandle {
|
|
36
|
+
/** @internal */
|
|
37
|
+
constructor(messageStream, control, sandbox, options) {
|
|
38
|
+
Object.defineProperty(this, "_stream", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: void 0
|
|
43
|
+
});
|
|
44
|
+
Object.defineProperty(this, "_control", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: void 0
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(this, "_sandbox", {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
writable: true,
|
|
54
|
+
value: void 0
|
|
55
|
+
});
|
|
56
|
+
Object.defineProperty(this, "_commandId", {
|
|
57
|
+
enumerable: true,
|
|
58
|
+
configurable: true,
|
|
59
|
+
writable: true,
|
|
60
|
+
value: null
|
|
61
|
+
});
|
|
62
|
+
Object.defineProperty(this, "_pid", {
|
|
63
|
+
enumerable: true,
|
|
64
|
+
configurable: true,
|
|
65
|
+
writable: true,
|
|
66
|
+
value: null
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(this, "_result", {
|
|
69
|
+
enumerable: true,
|
|
70
|
+
configurable: true,
|
|
71
|
+
writable: true,
|
|
72
|
+
value: null
|
|
73
|
+
});
|
|
74
|
+
Object.defineProperty(this, "_stdoutParts", {
|
|
75
|
+
enumerable: true,
|
|
76
|
+
configurable: true,
|
|
77
|
+
writable: true,
|
|
78
|
+
value: []
|
|
79
|
+
});
|
|
80
|
+
Object.defineProperty(this, "_stderrParts", {
|
|
81
|
+
enumerable: true,
|
|
82
|
+
configurable: true,
|
|
83
|
+
writable: true,
|
|
84
|
+
value: []
|
|
85
|
+
});
|
|
86
|
+
Object.defineProperty(this, "_exhausted", {
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
writable: true,
|
|
90
|
+
value: false
|
|
91
|
+
});
|
|
92
|
+
Object.defineProperty(this, "_lastStdoutOffset", {
|
|
93
|
+
enumerable: true,
|
|
94
|
+
configurable: true,
|
|
95
|
+
writable: true,
|
|
96
|
+
value: void 0
|
|
97
|
+
});
|
|
98
|
+
Object.defineProperty(this, "_lastStderrOffset", {
|
|
99
|
+
enumerable: true,
|
|
100
|
+
configurable: true,
|
|
101
|
+
writable: true,
|
|
102
|
+
value: void 0
|
|
103
|
+
});
|
|
104
|
+
Object.defineProperty(this, "_started", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
writable: true,
|
|
108
|
+
value: void 0
|
|
109
|
+
});
|
|
110
|
+
this._stream = messageStream;
|
|
111
|
+
this._control = control;
|
|
112
|
+
this._sandbox = sandbox;
|
|
113
|
+
this._lastStdoutOffset = options?.stdoutOffset ?? 0;
|
|
114
|
+
this._lastStderrOffset = options?.stderrOffset ?? 0;
|
|
115
|
+
// New executions (no commandId): _ensureStarted reads "started".
|
|
116
|
+
// Reconnections (commandId set): skip since reconnect streams
|
|
117
|
+
// don't send a "started" message.
|
|
118
|
+
if (options?.commandId) {
|
|
119
|
+
this._commandId = options.commandId;
|
|
120
|
+
this._started = true;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this._started = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Read the 'started' message to populate commandId and pid.
|
|
128
|
+
*
|
|
129
|
+
* Must be called (and awaited) before iterating for new executions.
|
|
130
|
+
*/
|
|
131
|
+
async _ensureStarted() {
|
|
132
|
+
if (this._started)
|
|
133
|
+
return;
|
|
134
|
+
const firstResult = await this._stream.next();
|
|
135
|
+
if (firstResult.done) {
|
|
136
|
+
throw new errors_js_1.LangSmithSandboxOperationError("Command stream ended before 'started' message", "command");
|
|
137
|
+
}
|
|
138
|
+
const firstMsg = firstResult.value;
|
|
139
|
+
if (firstMsg.type !== "started") {
|
|
140
|
+
throw new errors_js_1.LangSmithSandboxOperationError(`Expected 'started' message, got '${firstMsg.type}'`, "command");
|
|
141
|
+
}
|
|
142
|
+
this._commandId = firstMsg.command_id ?? null;
|
|
143
|
+
this._pid = firstMsg.pid ?? null;
|
|
144
|
+
this._started = true;
|
|
145
|
+
}
|
|
146
|
+
/** The server-assigned command ID. Available after _ensureStarted(). */
|
|
147
|
+
get commandId() {
|
|
148
|
+
return this._commandId;
|
|
149
|
+
}
|
|
150
|
+
/** The process ID on the sandbox. Available after _ensureStarted(). */
|
|
151
|
+
get pid() {
|
|
152
|
+
return this._pid;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The final execution result. Drains the stream if not already exhausted.
|
|
156
|
+
*/
|
|
157
|
+
get result() {
|
|
158
|
+
return this._getResult();
|
|
159
|
+
}
|
|
160
|
+
async _getResult() {
|
|
161
|
+
if (this._result === null) {
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
163
|
+
for await (const _ of this) {
|
|
164
|
+
// drain
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (this._result === null) {
|
|
168
|
+
throw new errors_js_1.LangSmithSandboxOperationError("Command stream ended without exit message", "command");
|
|
169
|
+
}
|
|
170
|
+
return this._result;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Iterate over output chunks from the current stream (no reconnect).
|
|
174
|
+
*/
|
|
175
|
+
async *_iterStream() {
|
|
176
|
+
await this._ensureStarted();
|
|
177
|
+
if (this._exhausted)
|
|
178
|
+
return;
|
|
179
|
+
for await (const msg of this._stream) {
|
|
180
|
+
const msgType = msg.type;
|
|
181
|
+
if (msgType === "stdout" || msgType === "stderr") {
|
|
182
|
+
const chunk = {
|
|
183
|
+
stream: msgType,
|
|
184
|
+
data: msg.data,
|
|
185
|
+
offset: msg.offset ?? 0,
|
|
186
|
+
};
|
|
187
|
+
if (msgType === "stdout") {
|
|
188
|
+
this._stdoutParts.push(msg.data);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
this._stderrParts.push(msg.data);
|
|
192
|
+
}
|
|
193
|
+
yield chunk;
|
|
194
|
+
}
|
|
195
|
+
else if (msgType === "exit") {
|
|
196
|
+
this._result = {
|
|
197
|
+
stdout: this._stdoutParts.join(""),
|
|
198
|
+
stderr: this._stderrParts.join(""),
|
|
199
|
+
exit_code: msg.exit_code ?? -1,
|
|
200
|
+
};
|
|
201
|
+
this._exhausted = true;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
this._exhausted = true;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Async iterate over output chunks with auto-reconnect on transient errors.
|
|
209
|
+
*
|
|
210
|
+
* Reconnect strategy:
|
|
211
|
+
* - 1001 Going Away (hot-reload): immediate reconnect, no delay
|
|
212
|
+
* - Other SandboxConnectionError: exponential backoff (0.5s, 1s, 2s...)
|
|
213
|
+
* - After kill(): no reconnect, error propagates
|
|
214
|
+
*/
|
|
215
|
+
async *[Symbol.asyncIterator]() {
|
|
216
|
+
let reconnectAttempts = 0;
|
|
217
|
+
while (true) {
|
|
218
|
+
try {
|
|
219
|
+
for await (const chunk of this._iterStream()) {
|
|
220
|
+
reconnectAttempts = 0; // Reset on successful data
|
|
221
|
+
if (chunk.stream === "stdout") {
|
|
222
|
+
this._lastStdoutOffset =
|
|
223
|
+
chunk.offset + new TextEncoder().encode(chunk.data).length;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
this._lastStderrOffset =
|
|
227
|
+
chunk.offset + new TextEncoder().encode(chunk.data).length;
|
|
228
|
+
}
|
|
229
|
+
yield chunk;
|
|
230
|
+
}
|
|
231
|
+
return; // Stream ended normally (exit message received)
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
const eName = e != null && typeof e === "object" ? e.name : "";
|
|
235
|
+
if (eName !== "LangSmithSandboxConnectionError" &&
|
|
236
|
+
eName !== "LangSmithSandboxServerReloadError") {
|
|
237
|
+
throw e;
|
|
238
|
+
}
|
|
239
|
+
if (this._control && this._control.killed) {
|
|
240
|
+
throw e;
|
|
241
|
+
}
|
|
242
|
+
reconnectAttempts++;
|
|
243
|
+
if (reconnectAttempts > CommandHandle.MAX_AUTO_RECONNECTS) {
|
|
244
|
+
throw new errors_js_1.LangSmithSandboxConnectionError(`Lost connection ${reconnectAttempts} times in succession, giving up`);
|
|
245
|
+
}
|
|
246
|
+
const isHotReload = eName === "LangSmithSandboxServerReloadError";
|
|
247
|
+
if (!isHotReload) {
|
|
248
|
+
const delay = Math.min(CommandHandle.BACKOFF_BASE * 2 ** (reconnectAttempts - 1), CommandHandle.BACKOFF_MAX);
|
|
249
|
+
await new Promise((r) => setTimeout(r, delay * 1000));
|
|
250
|
+
}
|
|
251
|
+
if (this._commandId === null) {
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
254
|
+
const newHandle = await this._sandbox.reconnect(this._commandId, {
|
|
255
|
+
stdoutOffset: this._lastStdoutOffset,
|
|
256
|
+
stderrOffset: this._lastStderrOffset,
|
|
257
|
+
});
|
|
258
|
+
this._stream = newHandle._stream;
|
|
259
|
+
this._control = newHandle._control;
|
|
260
|
+
this._exhausted = false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Send a kill signal to the running command (SIGKILL).
|
|
266
|
+
*
|
|
267
|
+
* The server kills the entire process group. The stream will
|
|
268
|
+
* subsequently yield an exit message with a non-zero exit code.
|
|
269
|
+
*/
|
|
270
|
+
kill() {
|
|
271
|
+
if (this._control) {
|
|
272
|
+
this._control.sendKill();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Write data to the command's stdin.
|
|
277
|
+
*/
|
|
278
|
+
sendInput(data) {
|
|
279
|
+
if (this._control) {
|
|
280
|
+
this._control.sendInput(data);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** Last known stdout byte offset (for manual reconnection). */
|
|
284
|
+
get lastStdoutOffset() {
|
|
285
|
+
return this._lastStdoutOffset;
|
|
286
|
+
}
|
|
287
|
+
/** Last known stderr byte offset (for manual reconnection). */
|
|
288
|
+
get lastStderrOffset() {
|
|
289
|
+
return this._lastStderrOffset;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Reconnect to this command from the last known offsets.
|
|
293
|
+
*
|
|
294
|
+
* Returns a new CommandHandle that resumes output from where this one
|
|
295
|
+
* left off.
|
|
296
|
+
*/
|
|
297
|
+
async reconnect() {
|
|
298
|
+
if (this._commandId === null) {
|
|
299
|
+
throw new errors_js_1.LangSmithSandboxOperationError("Cannot reconnect: command ID not available", "reconnect");
|
|
300
|
+
}
|
|
301
|
+
return this._sandbox.reconnect(this._commandId, {
|
|
302
|
+
stdoutOffset: this._lastStdoutOffset,
|
|
303
|
+
stderrOffset: this._lastStderrOffset,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
exports.CommandHandle = CommandHandle;
|
|
308
|
+
Object.defineProperty(CommandHandle, "MAX_AUTO_RECONNECTS", {
|
|
309
|
+
enumerable: true,
|
|
310
|
+
configurable: true,
|
|
311
|
+
writable: true,
|
|
312
|
+
value: 5
|
|
313
|
+
});
|
|
314
|
+
Object.defineProperty(CommandHandle, "BACKOFF_BASE", {
|
|
315
|
+
enumerable: true,
|
|
316
|
+
configurable: true,
|
|
317
|
+
writable: true,
|
|
318
|
+
value: 0.5
|
|
319
|
+
}); // seconds
|
|
320
|
+
Object.defineProperty(CommandHandle, "BACKOFF_MAX", {
|
|
321
|
+
enumerable: true,
|
|
322
|
+
configurable: true,
|
|
323
|
+
writable: true,
|
|
324
|
+
value: 8.0
|
|
325
|
+
}); // seconds
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommandHandle - async handle to a running command with streaming output
|
|
3
|
+
* and auto-reconnect.
|
|
4
|
+
*
|
|
5
|
+
* Port of Python's AsyncCommandHandle to TypeScript.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExecutionResult, OutputChunk } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Async handle to a running command with streaming output and auto-reconnect.
|
|
10
|
+
*
|
|
11
|
+
* Async iterable, yielding OutputChunk objects (stdout and stderr interleaved
|
|
12
|
+
* in arrival order). Access .result after iteration to get the full
|
|
13
|
+
* ExecutionResult.
|
|
14
|
+
*
|
|
15
|
+
* Auto-reconnect behavior:
|
|
16
|
+
* - Server hot-reload (1001 Going Away): reconnect immediately
|
|
17
|
+
* - Network error / unexpected close: reconnect with exponential backoff
|
|
18
|
+
* - User called kill(): do NOT reconnect (propagate error)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const handle = await sandbox.run("make build", { timeout: 600, wait: false });
|
|
23
|
+
*
|
|
24
|
+
* for await (const chunk of handle) { // auto-reconnects on transient errors
|
|
25
|
+
* process.stdout.write(chunk.data);
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* const result = await handle.result;
|
|
29
|
+
* console.log(`Exit code: ${result.exit_code}`);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare class CommandHandle {
|
|
33
|
+
static MAX_AUTO_RECONNECTS: number;
|
|
34
|
+
static BACKOFF_BASE: number;
|
|
35
|
+
static BACKOFF_MAX: number;
|
|
36
|
+
private _stream;
|
|
37
|
+
private _control;
|
|
38
|
+
private _sandbox;
|
|
39
|
+
private _commandId;
|
|
40
|
+
private _pid;
|
|
41
|
+
private _result;
|
|
42
|
+
private _stdoutParts;
|
|
43
|
+
private _stderrParts;
|
|
44
|
+
private _exhausted;
|
|
45
|
+
private _lastStdoutOffset;
|
|
46
|
+
private _lastStderrOffset;
|
|
47
|
+
private _started;
|
|
48
|
+
/**
|
|
49
|
+
* Read the 'started' message to populate commandId and pid.
|
|
50
|
+
*
|
|
51
|
+
* Must be called (and awaited) before iterating for new executions.
|
|
52
|
+
*/
|
|
53
|
+
_ensureStarted(): Promise<void>;
|
|
54
|
+
/** The server-assigned command ID. Available after _ensureStarted(). */
|
|
55
|
+
get commandId(): string | null;
|
|
56
|
+
/** The process ID on the sandbox. Available after _ensureStarted(). */
|
|
57
|
+
get pid(): number | null;
|
|
58
|
+
/**
|
|
59
|
+
* The final execution result. Drains the stream if not already exhausted.
|
|
60
|
+
*/
|
|
61
|
+
get result(): Promise<ExecutionResult>;
|
|
62
|
+
private _getResult;
|
|
63
|
+
/**
|
|
64
|
+
* Iterate over output chunks from the current stream (no reconnect).
|
|
65
|
+
*/
|
|
66
|
+
private _iterStream;
|
|
67
|
+
/**
|
|
68
|
+
* Async iterate over output chunks with auto-reconnect on transient errors.
|
|
69
|
+
*
|
|
70
|
+
* Reconnect strategy:
|
|
71
|
+
* - 1001 Going Away (hot-reload): immediate reconnect, no delay
|
|
72
|
+
* - Other SandboxConnectionError: exponential backoff (0.5s, 1s, 2s...)
|
|
73
|
+
* - After kill(): no reconnect, error propagates
|
|
74
|
+
*/
|
|
75
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<OutputChunk>;
|
|
76
|
+
/**
|
|
77
|
+
* Send a kill signal to the running command (SIGKILL).
|
|
78
|
+
*
|
|
79
|
+
* The server kills the entire process group. The stream will
|
|
80
|
+
* subsequently yield an exit message with a non-zero exit code.
|
|
81
|
+
*/
|
|
82
|
+
kill(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Write data to the command's stdin.
|
|
85
|
+
*/
|
|
86
|
+
sendInput(data: string): void;
|
|
87
|
+
/** Last known stdout byte offset (for manual reconnection). */
|
|
88
|
+
get lastStdoutOffset(): number;
|
|
89
|
+
/** Last known stderr byte offset (for manual reconnection). */
|
|
90
|
+
get lastStderrOffset(): number;
|
|
91
|
+
/**
|
|
92
|
+
* Reconnect to this command from the last known offsets.
|
|
93
|
+
*
|
|
94
|
+
* Returns a new CommandHandle that resumes output from where this one
|
|
95
|
+
* left off.
|
|
96
|
+
*/
|
|
97
|
+
reconnect(): Promise<CommandHandle>;
|
|
98
|
+
}
|