@hachej/boring-agent 0.1.30 → 0.1.32
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/{DebugDrawer-MCYZ4AWZ.js → DebugDrawer-PX2PHXJH.js} +1 -1
- package/dist/chunk-IDRC4SFU.js +85 -0
- package/dist/{chunk-MMJA3QON.js → chunk-UTUXVT2K.js} +33 -3
- package/dist/front/index.d.ts +47 -3
- package/dist/front/index.js +736 -304
- package/dist/front/styles.css +40 -23
- package/dist/{harness-CQ0uw6xW.d.ts → harness-CtTHLQyy.d.ts} +1 -1
- package/dist/server/index.d.ts +18 -12
- package/dist/server/index.js +1108 -406
- package/dist/shared/index.d.ts +2 -2
- package/docs/ERROR_CODES.md +38 -2
- package/package.json +5 -3
package/dist/server/index.js
CHANGED
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
safeCapture,
|
|
4
4
|
validateTool
|
|
5
5
|
} from "../chunk-HDOSWEXG.js";
|
|
6
|
+
import {
|
|
7
|
+
dropEmptyAssistantUiMessages,
|
|
8
|
+
sanitizeUiMessages
|
|
9
|
+
} from "../chunk-IDRC4SFU.js";
|
|
6
10
|
import {
|
|
7
11
|
ErrorCode
|
|
8
12
|
} from "../chunk-UBWQ5LT4.js";
|
|
@@ -100,23 +104,23 @@ function withWorkspacePythonEnv(opts) {
|
|
|
100
104
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
101
105
|
var DEFAULT_MAX_OUTPUT_BYTES = 1048576;
|
|
102
106
|
var TERMINATION_GRACE_MS = 5e3;
|
|
103
|
-
function appendOutput(chunks,
|
|
107
|
+
function appendOutput(chunks, chunk3, state, onChunk) {
|
|
104
108
|
const remaining = state.maxBytes - state.capturedBytes;
|
|
105
109
|
if (remaining <= 0) {
|
|
106
110
|
state.truncated = true;
|
|
107
111
|
return;
|
|
108
112
|
}
|
|
109
|
-
if (
|
|
110
|
-
const partial =
|
|
113
|
+
if (chunk3.length > remaining) {
|
|
114
|
+
const partial = chunk3.subarray(0, remaining);
|
|
111
115
|
chunks.push(partial);
|
|
112
116
|
state.capturedBytes += remaining;
|
|
113
117
|
state.truncated = true;
|
|
114
118
|
onChunk?.(new Uint8Array(partial));
|
|
115
119
|
return;
|
|
116
120
|
}
|
|
117
|
-
chunks.push(
|
|
118
|
-
state.capturedBytes +=
|
|
119
|
-
onChunk?.(new Uint8Array(
|
|
121
|
+
chunks.push(chunk3);
|
|
122
|
+
state.capturedBytes += chunk3.length;
|
|
123
|
+
onChunk?.(new Uint8Array(chunk3));
|
|
120
124
|
}
|
|
121
125
|
function terminateProcess(child, signal) {
|
|
122
126
|
const pid = child.pid;
|
|
@@ -157,7 +161,7 @@ function createDirectSandbox(opts = {}) {
|
|
|
157
161
|
const maxOutputBytes = opts2?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
158
162
|
const workspaceRoot = runtimeContext.runtimeCwd;
|
|
159
163
|
const cwd = opts2?.cwd ?? workspaceRoot;
|
|
160
|
-
return await new Promise((
|
|
164
|
+
return await new Promise((resolve11, reject) => {
|
|
161
165
|
const child = spawn(cmd, {
|
|
162
166
|
cwd,
|
|
163
167
|
env: withWorkspacePythonEnv({ workspaceRoot, env: opts2?.env }),
|
|
@@ -186,7 +190,7 @@ function createDirectSandbox(opts = {}) {
|
|
|
186
190
|
if (settled) return;
|
|
187
191
|
settled = true;
|
|
188
192
|
cleanup();
|
|
189
|
-
|
|
193
|
+
resolve11({
|
|
190
194
|
stdout: new Uint8Array(Buffer.concat(stdoutChunks)),
|
|
191
195
|
stderr: new Uint8Array(Buffer.concat(stderrChunks)),
|
|
192
196
|
exitCode: typeof exitCode === "number" ? exitCode : timedOut ? 124 : 1,
|
|
@@ -196,11 +200,11 @@ function createDirectSandbox(opts = {}) {
|
|
|
196
200
|
stderrEncoding: "utf-8"
|
|
197
201
|
});
|
|
198
202
|
};
|
|
199
|
-
child.stdout?.on("data", (
|
|
200
|
-
appendOutput(stdoutChunks,
|
|
203
|
+
child.stdout?.on("data", (chunk3) => {
|
|
204
|
+
appendOutput(stdoutChunks, chunk3, captureState, opts2?.onStdout);
|
|
201
205
|
});
|
|
202
|
-
child.stderr?.on("data", (
|
|
203
|
-
appendOutput(stderrChunks,
|
|
206
|
+
child.stderr?.on("data", (chunk3) => {
|
|
207
|
+
appendOutput(stderrChunks, chunk3, captureState, opts2?.onStderr);
|
|
204
208
|
});
|
|
205
209
|
child.on("error", (error) => {
|
|
206
210
|
if (settled) return;
|
|
@@ -625,23 +629,23 @@ function createNodeWorkspace(root, opts = {}) {
|
|
|
625
629
|
var DEFAULT_TIMEOUT_MS2 = BWRAP_TIMEOUT_SECONDS * 1e3;
|
|
626
630
|
var DEFAULT_MAX_OUTPUT_BYTES2 = 1048576;
|
|
627
631
|
var SANDBOX_HOME2 = "/workspace";
|
|
628
|
-
function appendOutput2(chunks,
|
|
632
|
+
function appendOutput2(chunks, chunk3, state, onChunk) {
|
|
629
633
|
const remaining = state.maxBytes - state.capturedBytes;
|
|
630
634
|
if (remaining <= 0) {
|
|
631
635
|
state.truncated = true;
|
|
632
636
|
return;
|
|
633
637
|
}
|
|
634
|
-
if (
|
|
635
|
-
const partial =
|
|
638
|
+
if (chunk3.length > remaining) {
|
|
639
|
+
const partial = chunk3.subarray(0, remaining);
|
|
636
640
|
chunks.push(partial);
|
|
637
641
|
state.capturedBytes += remaining;
|
|
638
642
|
state.truncated = true;
|
|
639
643
|
onChunk?.(new Uint8Array(partial));
|
|
640
644
|
return;
|
|
641
645
|
}
|
|
642
|
-
chunks.push(
|
|
643
|
-
state.capturedBytes +=
|
|
644
|
-
onChunk?.(new Uint8Array(
|
|
646
|
+
chunks.push(chunk3);
|
|
647
|
+
state.capturedBytes += chunk3.length;
|
|
648
|
+
onChunk?.(new Uint8Array(chunk3));
|
|
645
649
|
}
|
|
646
650
|
function terminateProcess2(child, signal) {
|
|
647
651
|
const pid = child.pid;
|
|
@@ -788,7 +792,7 @@ function createBwrapSandbox(opts = {}) {
|
|
|
788
792
|
"-c",
|
|
789
793
|
cmd
|
|
790
794
|
];
|
|
791
|
-
return await new Promise((
|
|
795
|
+
return await new Promise((resolve11, reject) => {
|
|
792
796
|
const child = spawn2("bwrap", args, {
|
|
793
797
|
env: {
|
|
794
798
|
...withWorkspacePythonEnv({ workspaceRoot, env: opts2?.env, sandboxRoot: SANDBOX_HOME2 }),
|
|
@@ -818,7 +822,7 @@ function createBwrapSandbox(opts = {}) {
|
|
|
818
822
|
if (settled) return;
|
|
819
823
|
settled = true;
|
|
820
824
|
cleanup();
|
|
821
|
-
|
|
825
|
+
resolve11({
|
|
822
826
|
stdout: new Uint8Array(Buffer.concat(stdoutChunks)),
|
|
823
827
|
stderr: new Uint8Array(Buffer.concat(stderrChunks)),
|
|
824
828
|
exitCode: typeof exitCode === "number" ? exitCode : timedOut ? 124 : 1,
|
|
@@ -828,11 +832,11 @@ function createBwrapSandbox(opts = {}) {
|
|
|
828
832
|
stderrEncoding: "utf-8"
|
|
829
833
|
});
|
|
830
834
|
};
|
|
831
|
-
child.stdout?.on("data", (
|
|
832
|
-
appendOutput2(stdoutChunks,
|
|
835
|
+
child.stdout?.on("data", (chunk3) => {
|
|
836
|
+
appendOutput2(stdoutChunks, chunk3, captureState, opts2?.onStdout);
|
|
833
837
|
});
|
|
834
|
-
child.stderr?.on("data", (
|
|
835
|
-
appendOutput2(stderrChunks,
|
|
838
|
+
child.stderr?.on("data", (chunk3) => {
|
|
839
|
+
appendOutput2(stderrChunks, chunk3, captureState, opts2?.onStderr);
|
|
836
840
|
});
|
|
837
841
|
child.on("error", (error) => {
|
|
838
842
|
if (settled) return;
|
|
@@ -1618,7 +1622,7 @@ async function prepareVercelDeploymentSnapshot(opts) {
|
|
|
1618
1622
|
}
|
|
1619
1623
|
|
|
1620
1624
|
// src/server/http/routes/file.ts
|
|
1621
|
-
import { dirname as dirname5, extname, relative as relative4 } from "path/posix";
|
|
1625
|
+
import { dirname as dirname5, extname as extname2, relative as relative4 } from "path/posix";
|
|
1622
1626
|
|
|
1623
1627
|
// src/server/http/middleware.ts
|
|
1624
1628
|
var ERROR_CODE_AUTH_REQUIRED = "auth_required";
|
|
@@ -1783,6 +1787,211 @@ function createLogger(prefix) {
|
|
|
1783
1787
|
};
|
|
1784
1788
|
}
|
|
1785
1789
|
|
|
1790
|
+
// src/server/http/routes/fileRecords.ts
|
|
1791
|
+
import { extname } from "path/posix";
|
|
1792
|
+
var MAX_RECORD_FILE_BYTES = 2 * 1024 * 1024;
|
|
1793
|
+
var MAX_RECORD_ROWS_SCANNED = 1e4;
|
|
1794
|
+
var MAX_RECORD_ROWS_RETURNED = 100;
|
|
1795
|
+
var DEFAULT_RECORD_ROWS_RETURNED = 50;
|
|
1796
|
+
var MAX_RECORD_OUTPUT_BYTES = 512 * 1024;
|
|
1797
|
+
var MAX_RECORD_QUERY_LENGTH = 256;
|
|
1798
|
+
var MAX_RECORD_COLUMNS = 100;
|
|
1799
|
+
var MAX_RECORD_COLUMN_SAMPLE_ROWS = 100;
|
|
1800
|
+
var FileRecordsValidationError = class extends Error {
|
|
1801
|
+
field;
|
|
1802
|
+
constructor(message, field) {
|
|
1803
|
+
super(message);
|
|
1804
|
+
this.name = "FileRecordsValidationError";
|
|
1805
|
+
this.field = field;
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
function firstQueryValue(value) {
|
|
1809
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1810
|
+
}
|
|
1811
|
+
function requireStringQueryParam(value, field) {
|
|
1812
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1813
|
+
throw new FileRecordsValidationError(`${field} is required`, field);
|
|
1814
|
+
}
|
|
1815
|
+
if (value.includes("\0")) {
|
|
1816
|
+
throw new FileRecordsValidationError("null bytes not allowed", field);
|
|
1817
|
+
}
|
|
1818
|
+
return value;
|
|
1819
|
+
}
|
|
1820
|
+
function parseNonNegativeInteger(value, field, fallback, max = MAX_RECORD_ROWS_SCANNED) {
|
|
1821
|
+
const raw = firstQueryValue(value);
|
|
1822
|
+
if (raw === void 0) return fallback;
|
|
1823
|
+
const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
|
|
1824
|
+
if (!/^\d+$/.test(text)) {
|
|
1825
|
+
throw new FileRecordsValidationError(`${field} must be a non-negative integer`, field);
|
|
1826
|
+
}
|
|
1827
|
+
const parsed = Number(text);
|
|
1828
|
+
if (!Number.isSafeInteger(parsed) || parsed > max) {
|
|
1829
|
+
throw new FileRecordsValidationError(`${field} is too large`, field);
|
|
1830
|
+
}
|
|
1831
|
+
return parsed;
|
|
1832
|
+
}
|
|
1833
|
+
function parseLimit(value) {
|
|
1834
|
+
const raw = firstQueryValue(value);
|
|
1835
|
+
if (raw === void 0) return DEFAULT_RECORD_ROWS_RETURNED;
|
|
1836
|
+
const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
|
|
1837
|
+
if (!/^\d+$/.test(text) || Number(text) < 1) {
|
|
1838
|
+
throw new FileRecordsValidationError("limit must be a positive integer", "limit");
|
|
1839
|
+
}
|
|
1840
|
+
return Math.min(Number(text), MAX_RECORD_ROWS_RETURNED);
|
|
1841
|
+
}
|
|
1842
|
+
function parseFileRecordsRequest(query) {
|
|
1843
|
+
const path4 = requireStringQueryParam(firstQueryValue(query.path), "path");
|
|
1844
|
+
const recordSet = firstQueryValue(query.recordSet);
|
|
1845
|
+
if (recordSet !== void 0 && String(recordSet).trim() !== "") {
|
|
1846
|
+
throw new FileRecordsValidationError("recordSet is not supported for file records in v1", "recordSet");
|
|
1847
|
+
}
|
|
1848
|
+
const offset = parseNonNegativeInteger(query.offset, "offset", 0);
|
|
1849
|
+
const limit = parseLimit(query.limit);
|
|
1850
|
+
const rawQ = firstQueryValue(query.q);
|
|
1851
|
+
const q = typeof rawQ === "string" ? rawQ.trim() : rawQ === void 0 ? "" : String(rawQ).trim();
|
|
1852
|
+
if (q.length > MAX_RECORD_QUERY_LENGTH) {
|
|
1853
|
+
throw new FileRecordsValidationError("q is too long", "q");
|
|
1854
|
+
}
|
|
1855
|
+
return { path: path4, offset, limit, q: q ? q.toLowerCase() : null };
|
|
1856
|
+
}
|
|
1857
|
+
function detectRecordsFormat(path4, content) {
|
|
1858
|
+
const ext = extname(path4).toLowerCase();
|
|
1859
|
+
if (ext === ".ndjson" || ext === ".jsonl") return "ndjson";
|
|
1860
|
+
if (ext === ".csv") return "csv";
|
|
1861
|
+
if (ext === ".json") return "json-array";
|
|
1862
|
+
const trimmed = content.trimStart();
|
|
1863
|
+
if (trimmed.startsWith("[")) return "json-array";
|
|
1864
|
+
if (trimmed.startsWith("{")) return "ndjson";
|
|
1865
|
+
return null;
|
|
1866
|
+
}
|
|
1867
|
+
function normalizeRecord(row) {
|
|
1868
|
+
if (row && typeof row === "object" && !Array.isArray(row)) return row;
|
|
1869
|
+
return { value: row };
|
|
1870
|
+
}
|
|
1871
|
+
function scalarMatches(value, q) {
|
|
1872
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return false;
|
|
1873
|
+
return String(value).toLowerCase().includes(q);
|
|
1874
|
+
}
|
|
1875
|
+
function recordMatches(record, q) {
|
|
1876
|
+
if (!q) return true;
|
|
1877
|
+
return Object.values(record).some((value) => scalarMatches(value, q));
|
|
1878
|
+
}
|
|
1879
|
+
function inferValueType(value) {
|
|
1880
|
+
if (value === null || value === void 0) return "null";
|
|
1881
|
+
if (Array.isArray(value)) return "array";
|
|
1882
|
+
return typeof value;
|
|
1883
|
+
}
|
|
1884
|
+
function inferColumns(rows) {
|
|
1885
|
+
const typesByName = /* @__PURE__ */ new Map();
|
|
1886
|
+
for (const row of rows.slice(0, MAX_RECORD_COLUMN_SAMPLE_ROWS)) {
|
|
1887
|
+
for (const [name, value] of Object.entries(row)) {
|
|
1888
|
+
if (!typesByName.has(name)) {
|
|
1889
|
+
if (typesByName.size >= MAX_RECORD_COLUMNS) continue;
|
|
1890
|
+
typesByName.set(name, /* @__PURE__ */ new Set());
|
|
1891
|
+
}
|
|
1892
|
+
typesByName.get(name)?.add(inferValueType(value));
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return [...typesByName.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, types]) => ({ name, type: types.size === 1 ? [...types][0] ?? "unknown" : "mixed" }));
|
|
1896
|
+
}
|
|
1897
|
+
function parseJsonArrayRecords(content) {
|
|
1898
|
+
let parsed;
|
|
1899
|
+
try {
|
|
1900
|
+
parsed = JSON.parse(content);
|
|
1901
|
+
} catch {
|
|
1902
|
+
throw new FileRecordsValidationError("malformed JSON records file");
|
|
1903
|
+
}
|
|
1904
|
+
if (!Array.isArray(parsed)) throw new FileRecordsValidationError("JSON records file must contain an array");
|
|
1905
|
+
if (parsed.length > MAX_RECORD_ROWS_SCANNED) throw new FileRecordsValidationError("record scan limit exceeded");
|
|
1906
|
+
return parsed.map(normalizeRecord);
|
|
1907
|
+
}
|
|
1908
|
+
function parseNdjsonRecords(content) {
|
|
1909
|
+
const rows = [];
|
|
1910
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1911
|
+
if (!line.trim()) continue;
|
|
1912
|
+
if (rows.length >= MAX_RECORD_ROWS_SCANNED) throw new FileRecordsValidationError("record scan limit exceeded");
|
|
1913
|
+
try {
|
|
1914
|
+
rows.push(normalizeRecord(JSON.parse(line)));
|
|
1915
|
+
} catch {
|
|
1916
|
+
throw new FileRecordsValidationError("malformed NDJSON records file");
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return rows;
|
|
1920
|
+
}
|
|
1921
|
+
function parseCsvRows(content) {
|
|
1922
|
+
const rows = [];
|
|
1923
|
+
let row = [];
|
|
1924
|
+
let current = "";
|
|
1925
|
+
let quoted = false;
|
|
1926
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
1927
|
+
const char = content[i];
|
|
1928
|
+
if (char === '"') {
|
|
1929
|
+
if (quoted && content[i + 1] === '"') {
|
|
1930
|
+
current += '"';
|
|
1931
|
+
i += 1;
|
|
1932
|
+
} else {
|
|
1933
|
+
quoted = !quoted;
|
|
1934
|
+
}
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
if (char === "," && !quoted) {
|
|
1938
|
+
row.push(current);
|
|
1939
|
+
current = "";
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
if ((char === "\n" || char === "\r") && !quoted) {
|
|
1943
|
+
if (char === "\r" && content[i + 1] === "\n") i += 1;
|
|
1944
|
+
row.push(current);
|
|
1945
|
+
if (row.some((value) => value.length > 0)) rows.push(row);
|
|
1946
|
+
row = [];
|
|
1947
|
+
current = "";
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
current += char;
|
|
1951
|
+
}
|
|
1952
|
+
if (quoted) throw new FileRecordsValidationError("malformed CSV records file");
|
|
1953
|
+
row.push(current);
|
|
1954
|
+
if (row.some((value) => value.length > 0)) rows.push(row);
|
|
1955
|
+
return rows;
|
|
1956
|
+
}
|
|
1957
|
+
function parseCsvRecords(content) {
|
|
1958
|
+
const parsedRows = parseCsvRows(content);
|
|
1959
|
+
if (parsedRows.length === 0) return [];
|
|
1960
|
+
const headers = (parsedRows[0] ?? []).map((header) => header.trim());
|
|
1961
|
+
if (headers.some((header) => !header)) throw new FileRecordsValidationError("CSV records file must have a non-empty header row");
|
|
1962
|
+
const rows = [];
|
|
1963
|
+
for (const values of parsedRows.slice(1)) {
|
|
1964
|
+
if (rows.length >= MAX_RECORD_ROWS_SCANNED) throw new FileRecordsValidationError("record scan limit exceeded");
|
|
1965
|
+
const row = {};
|
|
1966
|
+
for (const [index, header] of headers.entries()) row[header] = values[index] ?? "";
|
|
1967
|
+
rows.push(row);
|
|
1968
|
+
}
|
|
1969
|
+
return rows;
|
|
1970
|
+
}
|
|
1971
|
+
function buildFileRecordsResult(args) {
|
|
1972
|
+
const format = detectRecordsFormat(args.path, args.content);
|
|
1973
|
+
if (!format) throw new FileRecordsValidationError("unsupported records file format");
|
|
1974
|
+
const allRows = format === "json-array" ? parseJsonArrayRecords(args.content) : format === "ndjson" ? parseNdjsonRecords(args.content) : parseCsvRecords(args.content);
|
|
1975
|
+
const matching = allRows.filter((row) => recordMatches(row, args.q));
|
|
1976
|
+
const rows = matching.slice(args.offset, args.offset + args.limit);
|
|
1977
|
+
const result = {
|
|
1978
|
+
source: { kind: "file", path: args.path, format },
|
|
1979
|
+
path: args.path,
|
|
1980
|
+
format,
|
|
1981
|
+
columns: inferColumns(matching),
|
|
1982
|
+
rows,
|
|
1983
|
+
total: matching.length,
|
|
1984
|
+
hasMore: args.offset + rows.length < matching.length,
|
|
1985
|
+
offset: args.offset,
|
|
1986
|
+
limit: args.limit,
|
|
1987
|
+
mtimeMs: args.mtimeMs
|
|
1988
|
+
};
|
|
1989
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") > MAX_RECORD_OUTPUT_BYTES) {
|
|
1990
|
+
throw new FileRecordsValidationError("record output limit exceeded");
|
|
1991
|
+
}
|
|
1992
|
+
return result;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1786
1995
|
// src/server/http/routes/file.ts
|
|
1787
1996
|
var log = createLogger("boring/workspace-settings");
|
|
1788
1997
|
var BORING_SETTINGS_PATH = ".boring/settings";
|
|
@@ -1881,7 +2090,7 @@ function normalizeUploadDir(value) {
|
|
|
1881
2090
|
return dir.replace(/\/+$/, "");
|
|
1882
2091
|
}
|
|
1883
2092
|
function extForUpload(filename, contentType) {
|
|
1884
|
-
const fromName =
|
|
2093
|
+
const fromName = extname2(filename).toLowerCase().replace(/^\./, "");
|
|
1885
2094
|
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
1886
2095
|
if (contentType === "image/jpeg") return "jpg";
|
|
1887
2096
|
if (contentType === "image/png") return "png";
|
|
@@ -1903,7 +2112,7 @@ function markdownUrlFor(sourcePath, assetPath) {
|
|
|
1903
2112
|
return rel && !rel.startsWith(".") ? rel : rel || assetPath;
|
|
1904
2113
|
}
|
|
1905
2114
|
function contentTypeForPath(path4) {
|
|
1906
|
-
switch (
|
|
2115
|
+
switch (extname2(path4).toLowerCase()) {
|
|
1907
2116
|
case ".avif":
|
|
1908
2117
|
return "image/avif";
|
|
1909
2118
|
case ".gif":
|
|
@@ -1932,6 +2141,11 @@ function contentTypeForPath(path4) {
|
|
|
1932
2141
|
return "application/octet-stream";
|
|
1933
2142
|
}
|
|
1934
2143
|
}
|
|
2144
|
+
function sendValidationError(reply, message, field) {
|
|
2145
|
+
return reply.code(400).send({
|
|
2146
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message, ...field ? { field } : {} }
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
1935
2149
|
function fileRoutes(app, opts, done) {
|
|
1936
2150
|
async function resolveWorkspace(request) {
|
|
1937
2151
|
if (opts.getWorkspace) return await opts.getWorkspace(request);
|
|
@@ -1961,6 +2175,36 @@ function fileRoutes(app, opts, done) {
|
|
|
1961
2175
|
return classifyError(err, reply, "file");
|
|
1962
2176
|
}
|
|
1963
2177
|
});
|
|
2178
|
+
app.get("/api/v1/files/records", async (request, reply) => {
|
|
2179
|
+
try {
|
|
2180
|
+
const parsed = parseFileRecordsRequest(request.query);
|
|
2181
|
+
const workspace = await resolveWorkspace(request);
|
|
2182
|
+
const stat11 = await workspace.stat(parsed.path);
|
|
2183
|
+
if (stat11.kind !== "file") {
|
|
2184
|
+
return sendValidationError(reply, "path is not a file", "path");
|
|
2185
|
+
}
|
|
2186
|
+
if (stat11.size > MAX_RECORD_FILE_BYTES) {
|
|
2187
|
+
return sendValidationError(reply, "records file is too large", "path");
|
|
2188
|
+
}
|
|
2189
|
+
const content = await workspace.readFile(parsed.path);
|
|
2190
|
+
if (Buffer.byteLength(content, "utf8") > MAX_RECORD_FILE_BYTES) {
|
|
2191
|
+
return sendValidationError(reply, "records file is too large", "path");
|
|
2192
|
+
}
|
|
2193
|
+
return buildFileRecordsResult({
|
|
2194
|
+
path: parsed.path,
|
|
2195
|
+
content,
|
|
2196
|
+
mtimeMs: stat11.mtimeMs,
|
|
2197
|
+
offset: parsed.offset,
|
|
2198
|
+
limit: parsed.limit,
|
|
2199
|
+
q: parsed.q
|
|
2200
|
+
});
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
if (err instanceof FileRecordsValidationError) {
|
|
2203
|
+
return sendValidationError(reply, err.message, err.field);
|
|
2204
|
+
}
|
|
2205
|
+
return classifyError(err, reply, "file");
|
|
2206
|
+
}
|
|
2207
|
+
});
|
|
1964
2208
|
app.get("/api/v1/files", async (request, reply) => {
|
|
1965
2209
|
const query = request.query;
|
|
1966
2210
|
const path4 = requireStringParam(query.path, "path", reply);
|
|
@@ -2874,10 +3118,13 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
|
|
|
2874
3118
|
};
|
|
2875
3119
|
}
|
|
2876
3120
|
|
|
2877
|
-
// src/server/
|
|
2878
|
-
import {
|
|
2879
|
-
import {
|
|
3121
|
+
// src/server/workspace/provisioning/packArtifact.ts
|
|
3122
|
+
import { execFile as execFile2 } from "child_process";
|
|
3123
|
+
import { mkdir as mkdir5, mkdtemp, rename as rename4 } from "fs/promises";
|
|
3124
|
+
import { dirname as dirname7, isAbsolute as isAbsolute5, join as join4 } from "path";
|
|
2880
3125
|
import { tmpdir } from "os";
|
|
3126
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3127
|
+
import { promisify as promisify2 } from "util";
|
|
2881
3128
|
|
|
2882
3129
|
// src/server/workspace/provisioning/errors.ts
|
|
2883
3130
|
var ProvisioningError = class extends Error {
|
|
@@ -2904,17 +3151,62 @@ function logProvisioning(logger, level, message, fields = {}) {
|
|
|
2904
3151
|
logger?.[level]?.(message, fields);
|
|
2905
3152
|
}
|
|
2906
3153
|
|
|
2907
|
-
// src/server/
|
|
2908
|
-
var
|
|
3154
|
+
// src/server/workspace/provisioning/packArtifact.ts
|
|
3155
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3156
|
+
async function packProvisioningArtifact(request) {
|
|
3157
|
+
const sourcePath = request.source instanceof URL ? fileURLToPath2(request.source) : request.source;
|
|
3158
|
+
await mkdir5(dirname7(request.outputPath), { recursive: true });
|
|
3159
|
+
if (request.kind === "node") {
|
|
3160
|
+
const { stdout } = await execFileAsync2("pnpm", [
|
|
3161
|
+
"--dir",
|
|
3162
|
+
sourcePath,
|
|
3163
|
+
"pack",
|
|
3164
|
+
"--pack-destination",
|
|
3165
|
+
dirname7(request.outputPath)
|
|
3166
|
+
], { maxBuffer: 1024 * 1024 * 20 });
|
|
3167
|
+
const packedName = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
3168
|
+
if (!packedName) throw new Error(`pnpm pack produced no artifact for ${sourcePath}`);
|
|
3169
|
+
const packedPath = isAbsolute5(packedName) ? packedName : join4(dirname7(request.outputPath), packedName);
|
|
3170
|
+
await rename4(packedPath, request.outputPath);
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
await execFileAsync2("tar", ["-czf", request.outputPath, "-C", sourcePath, "."], {
|
|
3174
|
+
maxBuffer: 1024 * 1024 * 20
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
2909
3177
|
function artifactExtension(kind) {
|
|
2910
3178
|
return kind === "node" ? ".tgz" : ".tar.gz";
|
|
2911
3179
|
}
|
|
2912
|
-
function
|
|
3180
|
+
function provisioningArtifactName(kind, id, fingerprint2) {
|
|
2913
3181
|
const safeId = id.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
2914
3182
|
const safeFingerprint = fingerprint2.replace(/^sha256:/, "");
|
|
2915
3183
|
const formatVersion = kind === "node" ? "pnpm-pack-v2" : "v1";
|
|
2916
3184
|
return `${safeId}-${formatVersion}-${safeFingerprint}${artifactExtension(kind)}`;
|
|
2917
3185
|
}
|
|
3186
|
+
async function resolveArtifactInstallSource(args) {
|
|
3187
|
+
const { kind, id, fingerprint: fingerprint2 } = args.opts;
|
|
3188
|
+
const name = provisioningArtifactName(kind, id, fingerprint2);
|
|
3189
|
+
const workspaceRel = `.boring-agent/tmp/${name}`;
|
|
3190
|
+
if (!await args.workspaceFs.exists(workspaceRel)) {
|
|
3191
|
+
const artifactDir = await mkdtemp(join4(tmpdir(), "boring-agent-artifact-"));
|
|
3192
|
+
const outputPath = join4(artifactDir, name);
|
|
3193
|
+
try {
|
|
3194
|
+
await args.prepareArtifact({ kind, id, fingerprint: fingerprint2, source: args.source, outputPath });
|
|
3195
|
+
await args.workspaceFs.copyFromHost(outputPath, workspaceRel);
|
|
3196
|
+
} catch (error) {
|
|
3197
|
+
throw toProvisioningError(
|
|
3198
|
+
ErrorCode.enum.PROVISIONING_ARTIFACT_FAILED,
|
|
3199
|
+
"adapter-artifact",
|
|
3200
|
+
error,
|
|
3201
|
+
{ runtime: kind, id, artifact: workspaceRel }
|
|
3202
|
+
);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
return `${args.runtimeTmpDir}/${name}`;
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
// src/server/sandbox/vercel-sandbox/provisioningAdapter.ts
|
|
3209
|
+
var VERCEL_PROVISIONING_CACHE_ROOT = "/tmp/boring-agent-cache";
|
|
2918
3210
|
function createVercelProvisioningAdapter(options) {
|
|
2919
3211
|
return {
|
|
2920
3212
|
mode: "vercel-sandbox",
|
|
@@ -2926,31 +3218,13 @@ function createVercelProvisioningAdapter(options) {
|
|
|
2926
3218
|
});
|
|
2927
3219
|
},
|
|
2928
3220
|
async resolveInstallSource(source, opts) {
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
await options.prepareArtifact({
|
|
2937
|
-
kind: opts.kind,
|
|
2938
|
-
id: opts.id,
|
|
2939
|
-
fingerprint: opts.fingerprint,
|
|
2940
|
-
source,
|
|
2941
|
-
outputPath
|
|
2942
|
-
});
|
|
2943
|
-
await options.workspaceFs.copyFromHost(outputPath, workspaceRel);
|
|
2944
|
-
} catch (error) {
|
|
2945
|
-
throw toProvisioningError(
|
|
2946
|
-
ErrorCode.enum.PROVISIONING_ARTIFACT_FAILED,
|
|
2947
|
-
"adapter-artifact",
|
|
2948
|
-
error,
|
|
2949
|
-
{ runtime: opts.kind, id: opts.id, artifact: workspaceRel }
|
|
2950
|
-
);
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
return runtimePath;
|
|
3221
|
+
return await resolveArtifactInstallSource({
|
|
3222
|
+
workspaceFs: options.workspaceFs,
|
|
3223
|
+
prepareArtifact: options.prepareArtifact,
|
|
3224
|
+
runtimeTmpDir: options.runtimeLayout.tmp,
|
|
3225
|
+
source,
|
|
3226
|
+
opts
|
|
3227
|
+
});
|
|
2954
3228
|
},
|
|
2955
3229
|
workspaceFs: options.workspaceFs,
|
|
2956
3230
|
getRuntimeCacheRoot() {
|
|
@@ -2962,8 +3236,8 @@ function createVercelProvisioningAdapter(options) {
|
|
|
2962
3236
|
// src/server/workspace/provisioning/fingerprint.ts
|
|
2963
3237
|
import { createHash as createHash4 } from "crypto";
|
|
2964
3238
|
import { constants as constants3 } from "fs";
|
|
2965
|
-
import { access as access3, mkdir as
|
|
2966
|
-
import { dirname as
|
|
3239
|
+
import { access as access3, mkdir as mkdir6, open, readFile as readFile5, rename as rename5, rm as rm2 } from "fs/promises";
|
|
3240
|
+
import { dirname as dirname8 } from "path";
|
|
2967
3241
|
var FINGERPRINT_RE = /^sha256:[a-f0-9]{64}$/;
|
|
2968
3242
|
function isValidFingerprint(value) {
|
|
2969
3243
|
return FINGERPRINT_RE.test(value.trim());
|
|
@@ -3153,13 +3427,13 @@ async function ensureNodeRuntime(options) {
|
|
|
3153
3427
|
}
|
|
3154
3428
|
|
|
3155
3429
|
// src/server/workspace/provisioning/python.ts
|
|
3156
|
-
import { dirname as
|
|
3157
|
-
import { fileURLToPath as
|
|
3430
|
+
import { dirname as dirname9, join as join6, relative as relative7, sep as sep4 } from "path";
|
|
3431
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3158
3432
|
var VENV_REL = ".boring-agent/venv";
|
|
3159
3433
|
var VENV_FINGERPRINT_REL = `${VENV_REL}/.fingerprint`;
|
|
3160
3434
|
var UV_BIN_REL = ".boring-agent/sdk/uv/bin/uv";
|
|
3161
3435
|
function sourceToPath(source) {
|
|
3162
|
-
return source instanceof URL ?
|
|
3436
|
+
return source instanceof URL ? fileURLToPath3(source) : source;
|
|
3163
3437
|
}
|
|
3164
3438
|
async function commandOutput2(adapter, command, args) {
|
|
3165
3439
|
const result = await adapter.exec(command, args);
|
|
@@ -3225,7 +3499,7 @@ function pythonInstallSource(spec) {
|
|
|
3225
3499
|
}
|
|
3226
3500
|
function sourceRootForPythonSpec(spec) {
|
|
3227
3501
|
if (spec.packageRoot) return spec.packageRoot;
|
|
3228
|
-
if (spec.projectFile) return
|
|
3502
|
+
if (spec.projectFile) return dirname9(sourceToPath(spec.projectFile));
|
|
3229
3503
|
return null;
|
|
3230
3504
|
}
|
|
3231
3505
|
function expectedPythonOutputs(paths, packages) {
|
|
@@ -3254,6 +3528,7 @@ async function shouldInstallPythonRuntime(options) {
|
|
|
3254
3528
|
return true;
|
|
3255
3529
|
}
|
|
3256
3530
|
for (const output of options.expectedOutputs) {
|
|
3531
|
+
if (output === options.runtimeLayout.venvPython) continue;
|
|
3257
3532
|
if (!await options.adapter.workspaceFs.exists(toWorkspaceRel2(options.runtimeLayout, output))) return true;
|
|
3258
3533
|
}
|
|
3259
3534
|
return false;
|
|
@@ -3345,11 +3620,11 @@ async function ensurePythonRuntime(options) {
|
|
|
3345
3620
|
// src/server/workspace/provisioning/skills.ts
|
|
3346
3621
|
import { stat as stat5 } from "fs/promises";
|
|
3347
3622
|
import { join as join7 } from "path";
|
|
3348
|
-
import { fileURLToPath as
|
|
3623
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
3349
3624
|
var GENERATED_SKILLS_REL = ".boring-agent/skills";
|
|
3350
3625
|
var USER_SKILLS_REL = ".agents/skills";
|
|
3351
3626
|
function sourceToPath2(source) {
|
|
3352
|
-
return source instanceof URL ?
|
|
3627
|
+
return source instanceof URL ? fileURLToPath4(source) : source;
|
|
3353
3628
|
}
|
|
3354
3629
|
function assertSafeSegment(kind, value) {
|
|
3355
3630
|
if (value.length === 0 || value.includes("\0") || value.includes("/") || value.includes("\\") || value === "." || value === "..") {
|
|
@@ -3391,9 +3666,9 @@ async function mirrorPluginSkills(options) {
|
|
|
3391
3666
|
import { basename, join as joinPath } from "path";
|
|
3392
3667
|
import { posix as posix2 } from "path";
|
|
3393
3668
|
import { readdir as readdir3, stat as stat6 } from "fs/promises";
|
|
3394
|
-
import { fileURLToPath as
|
|
3669
|
+
import { fileURLToPath as fileURLToPath5, pathToFileURL } from "url";
|
|
3395
3670
|
function sourceToPath3(source) {
|
|
3396
|
-
return source instanceof URL ?
|
|
3671
|
+
return source instanceof URL ? fileURLToPath5(source) : source;
|
|
3397
3672
|
}
|
|
3398
3673
|
function toWorkspaceRel3(...parts) {
|
|
3399
3674
|
return posix2.normalize(parts.filter(Boolean).join("/"));
|
|
@@ -3687,7 +3962,7 @@ async function provisionWorkspaceRuntime(opts) {
|
|
|
3687
3962
|
import { spawnSync } from "child_process";
|
|
3688
3963
|
|
|
3689
3964
|
// src/server/runtime/modes/direct.ts
|
|
3690
|
-
import { mkdir as
|
|
3965
|
+
import { mkdir as mkdir8 } from "fs/promises";
|
|
3691
3966
|
|
|
3692
3967
|
// src/server/runtime/createServerFileSearch.ts
|
|
3693
3968
|
var DEFAULT_LIMIT = 500;
|
|
@@ -3765,12 +4040,12 @@ async function copyTemplate(templatePath, workspaceRoot) {
|
|
|
3765
4040
|
|
|
3766
4041
|
// src/server/runtime/modes/provisioningAdapter.ts
|
|
3767
4042
|
import { spawn as spawn3 } from "child_process";
|
|
3768
|
-
import { cp as cp3, lstat as lstat3, mkdir as
|
|
3769
|
-
import { dirname as
|
|
3770
|
-
import { fileURLToPath as
|
|
4043
|
+
import { cp as cp3, lstat as lstat3, mkdir as mkdir7, readFile as readFile6, realpath as realpath3, rm as rm3, stat as stat7, writeFile as writeFile5 } from "fs/promises";
|
|
4044
|
+
import { dirname as dirname10, isAbsolute as isAbsolute6, relative as relative8, resolve as resolve6, sep as sep5 } from "path";
|
|
4045
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
3771
4046
|
var LOCAL_SANDBOX_WORKSPACE_ROOT = "/workspace";
|
|
3772
4047
|
function sourceToPath4(source) {
|
|
3773
|
-
return source instanceof URL ?
|
|
4048
|
+
return source instanceof URL ? fileURLToPath6(source) : source;
|
|
3774
4049
|
}
|
|
3775
4050
|
async function assertExistingInsideWorkspace(root, relPath, enforceSymlinkBoundary) {
|
|
3776
4051
|
const absPath = validatePath(root, relPath);
|
|
@@ -3788,9 +4063,9 @@ async function assertExistingInsideWorkspace(root, relPath, enforceSymlinkBounda
|
|
|
3788
4063
|
}
|
|
3789
4064
|
async function prepareWritablePath(root, relPath, enforceSymlinkBoundary) {
|
|
3790
4065
|
const absPath = validatePath(root, relPath);
|
|
3791
|
-
await
|
|
4066
|
+
await mkdir7(dirname10(absPath), { recursive: true });
|
|
3792
4067
|
if (enforceSymlinkBoundary) {
|
|
3793
|
-
await assertRealPathWithinWorkspace(root,
|
|
4068
|
+
await assertRealPathWithinWorkspace(root, dirname10(absPath));
|
|
3794
4069
|
}
|
|
3795
4070
|
try {
|
|
3796
4071
|
const targetStat = await lstat3(absPath);
|
|
@@ -3829,8 +4104,8 @@ async function spawnCommand(command, args, opts) {
|
|
|
3829
4104
|
stderr: Buffer.concat(stderr).toString("utf8")
|
|
3830
4105
|
});
|
|
3831
4106
|
};
|
|
3832
|
-
child.stdout?.on("data", (
|
|
3833
|
-
child.stderr?.on("data", (
|
|
4107
|
+
child.stdout?.on("data", (chunk3) => stdout.push(chunk3));
|
|
4108
|
+
child.stderr?.on("data", (chunk3) => stderr.push(chunk3));
|
|
3834
4109
|
child.on("error", settle);
|
|
3835
4110
|
child.on("close", (code) => {
|
|
3836
4111
|
if (code === 0) {
|
|
@@ -3854,7 +4129,7 @@ function defaultExecOptions(paths, opts) {
|
|
|
3854
4129
|
};
|
|
3855
4130
|
}
|
|
3856
4131
|
function mapWorkspacePathToLocalSandbox(paths, value) {
|
|
3857
|
-
const absolute =
|
|
4132
|
+
const absolute = isAbsolute6(value) ? value : resolve6(paths.workspaceRoot, value);
|
|
3858
4133
|
const relPath = relative8(paths.workspaceRoot, absolute);
|
|
3859
4134
|
if (relPath === "") return LOCAL_SANDBOX_WORKSPACE_ROOT;
|
|
3860
4135
|
if (relPath === ".." || relPath.startsWith(`..${sep5}`)) return value;
|
|
@@ -3868,33 +4143,18 @@ function mapEnvToLocalSandbox(paths, env) {
|
|
|
3868
4143
|
Object.entries(env).map(([key, value]) => [key, mapValueToLocalSandbox(paths, value)])
|
|
3869
4144
|
);
|
|
3870
4145
|
}
|
|
3871
|
-
function sanitizeInstallSourcePart(value) {
|
|
3872
|
-
const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3873
|
-
return sanitized.length > 0 ? sanitized : "source";
|
|
3874
|
-
}
|
|
3875
|
-
async function copyExternalSourceIntoWorkspace(paths, sourcePath, opts) {
|
|
3876
|
-
const fingerprint2 = opts.fingerprint.replace(/^sha256:/, "");
|
|
3877
|
-
const relTarget = `.boring-agent/tmp/${sanitizeInstallSourcePart(opts.kind)}-${sanitizeInstallSourcePart(opts.id)}-${sanitizeInstallSourcePart(fingerprint2)}-source`;
|
|
3878
|
-
const absTarget = validatePath(paths.workspaceRoot, relTarget);
|
|
3879
|
-
await rm3(absTarget, { recursive: true, force: true });
|
|
3880
|
-
await mkdir6(dirname9(absTarget), { recursive: true });
|
|
3881
|
-
await assertRealPathWithinWorkspace(paths.workspaceRoot, dirname9(absTarget));
|
|
3882
|
-
const sourceStat = await stat7(sourcePath);
|
|
3883
|
-
await cp3(sourcePath, absTarget, {
|
|
3884
|
-
recursive: sourceStat.isDirectory(),
|
|
3885
|
-
force: false,
|
|
3886
|
-
errorOnExist: true
|
|
3887
|
-
});
|
|
3888
|
-
return `${LOCAL_SANDBOX_WORKSPACE_ROOT}/${relTarget}`;
|
|
3889
|
-
}
|
|
3890
4146
|
function createWorkspaceFs(workspaceRoot, opts) {
|
|
3891
4147
|
const { enforceSymlinkBoundary } = opts;
|
|
3892
4148
|
return {
|
|
3893
4149
|
async exists(workspaceRelativePath) {
|
|
3894
|
-
const absPath =
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
4150
|
+
const absPath = validatePath(workspaceRoot, workspaceRelativePath);
|
|
4151
|
+
try {
|
|
4152
|
+
await stat7(absPath);
|
|
4153
|
+
return true;
|
|
4154
|
+
} catch (error) {
|
|
4155
|
+
if (error.code === "ENOENT") return false;
|
|
4156
|
+
throw error;
|
|
4157
|
+
}
|
|
3898
4158
|
},
|
|
3899
4159
|
async rm(workspaceRelativePath) {
|
|
3900
4160
|
const absPath = await assertExistingInsideWorkspace(workspaceRoot, workspaceRelativePath, enforceSymlinkBoundary);
|
|
@@ -3903,7 +4163,7 @@ function createWorkspaceFs(workspaceRoot, opts) {
|
|
|
3903
4163
|
},
|
|
3904
4164
|
async mkdir(workspaceRelativePath) {
|
|
3905
4165
|
const absPath = validatePath(workspaceRoot, workspaceRelativePath);
|
|
3906
|
-
await
|
|
4166
|
+
await mkdir7(absPath, { recursive: true });
|
|
3907
4167
|
if (enforceSymlinkBoundary) {
|
|
3908
4168
|
await assertRealPathWithinWorkspace(workspaceRoot, absPath);
|
|
3909
4169
|
}
|
|
@@ -3946,6 +4206,7 @@ function createDirectProvisioningAdapter(paths, runner = spawnCommand) {
|
|
|
3946
4206
|
}
|
|
3947
4207
|
function createLocalProvisioningAdapter(paths, runner = spawnCommand) {
|
|
3948
4208
|
const sourceMounts = /* @__PURE__ */ new Map();
|
|
4209
|
+
const workspaceFs = createWorkspaceFs(paths.workspaceRoot, { enforceSymlinkBoundary: true });
|
|
3949
4210
|
return {
|
|
3950
4211
|
mode: "local",
|
|
3951
4212
|
async exec(command, args, opts) {
|
|
@@ -3975,12 +4236,18 @@ function createLocalProvisioningAdapter(paths, runner = spawnCommand) {
|
|
|
3975
4236
|
const realSource = await realpath3(hostPath);
|
|
3976
4237
|
const relPath = relative8(realWorkspaceRoot, realSource);
|
|
3977
4238
|
if (relPath === "") return LOCAL_SANDBOX_WORKSPACE_ROOT;
|
|
3978
|
-
if (!relPath.startsWith("..") && !
|
|
4239
|
+
if (!relPath.startsWith("..") && !isAbsolute6(relPath)) {
|
|
3979
4240
|
return `${LOCAL_SANDBOX_WORKSPACE_ROOT}/${relPath.split(sep5).join("/")}`;
|
|
3980
4241
|
}
|
|
3981
|
-
return await
|
|
4242
|
+
return await resolveArtifactInstallSource({
|
|
4243
|
+
workspaceFs,
|
|
4244
|
+
prepareArtifact: packProvisioningArtifact,
|
|
4245
|
+
runtimeTmpDir: `${LOCAL_SANDBOX_WORKSPACE_ROOT}/.boring-agent/tmp`,
|
|
4246
|
+
source: realSource,
|
|
4247
|
+
opts
|
|
4248
|
+
});
|
|
3982
4249
|
},
|
|
3983
|
-
workspaceFs
|
|
4250
|
+
workspaceFs,
|
|
3984
4251
|
getRuntimeCacheRoot() {
|
|
3985
4252
|
return paths.cache;
|
|
3986
4253
|
}
|
|
@@ -3993,7 +4260,7 @@ var directModeAdapter = {
|
|
|
3993
4260
|
workspaceFsCapability: "strong",
|
|
3994
4261
|
createProvisioningAdapter: (runtimeLayout) => createDirectProvisioningAdapter(runtimeLayout),
|
|
3995
4262
|
async create(ctx) {
|
|
3996
|
-
await
|
|
4263
|
+
await mkdir8(ctx.workspaceRoot, { recursive: true });
|
|
3997
4264
|
await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
|
|
3998
4265
|
const runtimeContext = { runtimeCwd: ctx.workspaceRoot };
|
|
3999
4266
|
const workspace = createNodeWorkspace(ctx.workspaceRoot, { runtimeContext });
|
|
@@ -4010,7 +4277,7 @@ var directModeAdapter = {
|
|
|
4010
4277
|
};
|
|
4011
4278
|
|
|
4012
4279
|
// src/server/runtime/modes/local.ts
|
|
4013
|
-
import { mkdir as
|
|
4280
|
+
import { mkdir as mkdir9 } from "fs/promises";
|
|
4014
4281
|
var localModeAdapter = {
|
|
4015
4282
|
id: "local",
|
|
4016
4283
|
workspaceFsCapability: "strong",
|
|
@@ -4019,7 +4286,7 @@ var localModeAdapter = {
|
|
|
4019
4286
|
if (process.platform !== "linux") {
|
|
4020
4287
|
throw new Error("local mode requires Linux with bubblewrap");
|
|
4021
4288
|
}
|
|
4022
|
-
await
|
|
4289
|
+
await mkdir9(ctx.workspaceRoot, { recursive: true });
|
|
4023
4290
|
await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
|
|
4024
4291
|
const runtimeContext = { runtimeCwd: "/workspace" };
|
|
4025
4292
|
const workspace = createNodeWorkspace(ctx.workspaceRoot, { runtimeContext });
|
|
@@ -4039,11 +4306,9 @@ var localModeAdapter = {
|
|
|
4039
4306
|
};
|
|
4040
4307
|
|
|
4041
4308
|
// src/server/runtime/modes/vercel-sandbox.ts
|
|
4042
|
-
import {
|
|
4043
|
-
import {
|
|
4044
|
-
import {
|
|
4045
|
-
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
4046
|
-
import { promisify as promisify2 } from "util";
|
|
4309
|
+
import { readFile as readFile8, readdir as readdir5, stat as stat8 } from "fs/promises";
|
|
4310
|
+
import { join as join8 } from "path";
|
|
4311
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
4047
4312
|
import { Sandbox as VercelSandbox } from "@vercel/sandbox";
|
|
4048
4313
|
|
|
4049
4314
|
// src/server/sandbox/vercel-sandbox/createVercelSandboxExec.ts
|
|
@@ -4054,15 +4319,15 @@ var DEFAULT_MAX_OUTPUT_BYTES3 = 1048576;
|
|
|
4054
4319
|
var VERCEL_SANDBOX_DEFAULT_PATH = "/vercel/runtimes/node24/bin:/vercel/runtimes/node22/bin:/vercel/runtimes/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
4055
4320
|
function createStreamWritable(collector, shared, onChunk) {
|
|
4056
4321
|
return new Writable({
|
|
4057
|
-
write(
|
|
4322
|
+
write(chunk3, _enc, cb) {
|
|
4058
4323
|
const remaining = shared.maxBytes - shared.totalBytes;
|
|
4059
4324
|
if (remaining <= 0) {
|
|
4060
4325
|
shared.truncated = true;
|
|
4061
4326
|
cb();
|
|
4062
4327
|
return;
|
|
4063
4328
|
}
|
|
4064
|
-
if (
|
|
4065
|
-
const partial =
|
|
4329
|
+
if (chunk3.length > remaining) {
|
|
4330
|
+
const partial = chunk3.subarray(0, remaining);
|
|
4066
4331
|
collector.chunks.push(partial);
|
|
4067
4332
|
shared.totalBytes += remaining;
|
|
4068
4333
|
shared.truncated = true;
|
|
@@ -4070,9 +4335,9 @@ function createStreamWritable(collector, shared, onChunk) {
|
|
|
4070
4335
|
cb();
|
|
4071
4336
|
return;
|
|
4072
4337
|
}
|
|
4073
|
-
collector.chunks.push(
|
|
4074
|
-
shared.totalBytes +=
|
|
4075
|
-
onChunk?.(new Uint8Array(
|
|
4338
|
+
collector.chunks.push(chunk3);
|
|
4339
|
+
shared.totalBytes += chunk3.length;
|
|
4340
|
+
onChunk?.(new Uint8Array(chunk3));
|
|
4076
4341
|
cb();
|
|
4077
4342
|
}
|
|
4078
4343
|
});
|
|
@@ -4256,11 +4521,11 @@ async function buildTarGz(files) {
|
|
|
4256
4521
|
}
|
|
4257
4522
|
chunks.push(Buffer.alloc(1024));
|
|
4258
4523
|
const tarBuffer = Buffer.concat(chunks);
|
|
4259
|
-
return new Promise((
|
|
4524
|
+
return new Promise((resolve11, reject) => {
|
|
4260
4525
|
const gzip = createGzip({ level: 6 });
|
|
4261
4526
|
const gzChunks = [];
|
|
4262
|
-
gzip.on("data", (
|
|
4263
|
-
gzip.on("end", () =>
|
|
4527
|
+
gzip.on("data", (chunk3) => gzChunks.push(chunk3));
|
|
4528
|
+
gzip.on("end", () => resolve11(Buffer.concat(gzChunks)));
|
|
4264
4529
|
gzip.on("error", reject);
|
|
4265
4530
|
Readable.from(tarBuffer).pipe(gzip);
|
|
4266
4531
|
});
|
|
@@ -4300,7 +4565,6 @@ var ORPHAN_GUARD_MAX_IDLE_MS = 24 * 60 * 60 * 1e3;
|
|
|
4300
4565
|
var VERCEL_SANDBOX_TIMEOUT_MS_ENV = "BORING_AGENT_VERCEL_SANDBOX_TIMEOUT_MS";
|
|
4301
4566
|
var VERCEL_SANDBOX_RUNTIME_ENV = "BORING_AGENT_VERCEL_SANDBOX_RUNTIME";
|
|
4302
4567
|
var DEFAULT_VERCEL_SANDBOX_RUNTIME = "node24";
|
|
4303
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
4304
4568
|
function sandboxTelemetryProperties(ctx, extra = {}) {
|
|
4305
4569
|
return {
|
|
4306
4570
|
runtimeMode: "vercel-sandbox",
|
|
@@ -4379,6 +4643,13 @@ function createDefaultVercelClient(auth, opts = {}) {
|
|
|
4379
4643
|
}
|
|
4380
4644
|
};
|
|
4381
4645
|
}
|
|
4646
|
+
function buildWorkspaceRootSetupScript(remoteRoot, workspaceRoot) {
|
|
4647
|
+
return [
|
|
4648
|
+
`if [ -L ${workspaceRoot} ]; then rm -f ${workspaceRoot}; fi`,
|
|
4649
|
+
`mkdir -p ${remoteRoot}`,
|
|
4650
|
+
`if [ ${remoteRoot} != ${workspaceRoot} ]; then ln -sfn ${remoteRoot} ${workspaceRoot} 2>/dev/null || true; fi`
|
|
4651
|
+
].join("; ");
|
|
4652
|
+
}
|
|
4382
4653
|
async function ensureVercelWorkspaceRoot(sandbox) {
|
|
4383
4654
|
let rootCreated = false;
|
|
4384
4655
|
if (sandbox.fs?.mkdir) {
|
|
@@ -4397,7 +4668,7 @@ async function ensureVercelWorkspaceRoot(sandbox) {
|
|
|
4397
4668
|
}
|
|
4398
4669
|
const result = await sandbox.runCommand({
|
|
4399
4670
|
cmd: "sh",
|
|
4400
|
-
args: ["-c",
|
|
4671
|
+
args: ["-c", buildWorkspaceRootSetupScript(VERCEL_SANDBOX_REMOTE_ROOT, VERCEL_SANDBOX_WORKSPACE_ROOT)]
|
|
4401
4672
|
});
|
|
4402
4673
|
if ((result.exitCode ?? 1) !== 0) {
|
|
4403
4674
|
throw new Error(`failed to initialize ${VERCEL_SANDBOX_REMOTE_ROOT} (exit ${result.exitCode ?? "unknown"})`);
|
|
@@ -4441,28 +4712,7 @@ async function seedTemplateIntoVercelWorkspace(workspace, templatePath, logger)
|
|
|
4441
4712
|
});
|
|
4442
4713
|
}
|
|
4443
4714
|
function provisioningSourceToPath(source) {
|
|
4444
|
-
return source instanceof URL ?
|
|
4445
|
-
}
|
|
4446
|
-
async function prepareVercelProvisioningArtifact(request) {
|
|
4447
|
-
const sourcePath = provisioningSourceToPath(request.source);
|
|
4448
|
-
await mkdir9(dirname10(request.outputPath), { recursive: true });
|
|
4449
|
-
if (request.kind === "node") {
|
|
4450
|
-
const { stdout } = await execFileAsync2("pnpm", [
|
|
4451
|
-
"--dir",
|
|
4452
|
-
sourcePath,
|
|
4453
|
-
"pack",
|
|
4454
|
-
"--pack-destination",
|
|
4455
|
-
dirname10(request.outputPath)
|
|
4456
|
-
], { maxBuffer: 1024 * 1024 * 20 });
|
|
4457
|
-
const packedName = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
4458
|
-
if (!packedName) throw new Error(`pnpm pack produced no artifact for ${sourcePath}`);
|
|
4459
|
-
const packedPath = isAbsolute6(packedName) ? packedName : join8(dirname10(request.outputPath), packedName);
|
|
4460
|
-
await rename5(packedPath, request.outputPath);
|
|
4461
|
-
return;
|
|
4462
|
-
}
|
|
4463
|
-
await execFileAsync2("tar", ["-czf", request.outputPath, "-C", sourcePath, "."], {
|
|
4464
|
-
maxBuffer: 1024 * 1024 * 20
|
|
4465
|
-
});
|
|
4715
|
+
return source instanceof URL ? fileURLToPath7(source) : source;
|
|
4466
4716
|
}
|
|
4467
4717
|
function shellSingleQuote(value) {
|
|
4468
4718
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -4712,7 +4962,7 @@ function createVercelSandboxModeAdapter(opts = {}) {
|
|
|
4712
4962
|
}
|
|
4713
4963
|
return { stdout, stderr };
|
|
4714
4964
|
},
|
|
4715
|
-
prepareArtifact:
|
|
4965
|
+
prepareArtifact: packProvisioningArtifact
|
|
4716
4966
|
});
|
|
4717
4967
|
},
|
|
4718
4968
|
async create(ctx) {
|
|
@@ -4893,8 +5143,8 @@ import Fastify from "fastify";
|
|
|
4893
5143
|
|
|
4894
5144
|
// src/server/harness/pi-coding-agent/createHarness.ts
|
|
4895
5145
|
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
4896
|
-
import { mkdir as
|
|
4897
|
-
import { extname as
|
|
5146
|
+
import { mkdir as mkdir12, writeFile as writeFile7 } from "fs/promises";
|
|
5147
|
+
import { extname as extname3, join as join10 } from "path";
|
|
4898
5148
|
import {
|
|
4899
5149
|
createAgentSession,
|
|
4900
5150
|
SessionManager,
|
|
@@ -5175,17 +5425,16 @@ import {
|
|
|
5175
5425
|
readFile as readFile9,
|
|
5176
5426
|
stat as fsStat,
|
|
5177
5427
|
rm as rm4,
|
|
5178
|
-
mkdir as
|
|
5428
|
+
mkdir as mkdir11,
|
|
5179
5429
|
writeFile as writeFile6,
|
|
5180
5430
|
appendFile,
|
|
5181
5431
|
utimes
|
|
5182
5432
|
} from "fs/promises";
|
|
5183
5433
|
import { readFileSync, readdirSync } from "fs";
|
|
5184
|
-
import { join as join9 } from "path";
|
|
5434
|
+
import { join as join9, resolve as resolve7 } from "path";
|
|
5185
5435
|
import { homedir as homedir3 } from "os";
|
|
5186
5436
|
import {
|
|
5187
5437
|
parseSessionEntries,
|
|
5188
|
-
buildSessionContext,
|
|
5189
5438
|
CURRENT_SESSION_VERSION
|
|
5190
5439
|
} from "@mariozechner/pi-coding-agent";
|
|
5191
5440
|
function defaultSessionDir(cwd) {
|
|
@@ -5218,13 +5467,16 @@ var PiSessionStore = class {
|
|
|
5218
5467
|
async list(ctx) {
|
|
5219
5468
|
const files = await readdir6(this.sessionDir).catch(() => []);
|
|
5220
5469
|
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
5470
|
+
const filepaths = jsonlFiles.map((f) => join9(this.sessionDir, f));
|
|
5471
|
+
const referencedPiFiles = await this.referencedPiFiles(filepaths);
|
|
5472
|
+
const visibleFiles = filepaths.filter((filepath) => !referencedPiFiles.has(resolve7(filepath)));
|
|
5221
5473
|
const summaries = await Promise.all(
|
|
5222
|
-
|
|
5474
|
+
visibleFiles.map((filepath) => this.summarizeFile(filepath))
|
|
5223
5475
|
);
|
|
5224
5476
|
return summaries.filter((s) => s !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
5225
5477
|
}
|
|
5226
5478
|
async create(ctx, init) {
|
|
5227
|
-
await
|
|
5479
|
+
await mkdir11(this.sessionDir, { recursive: true });
|
|
5228
5480
|
const id = randomUUID();
|
|
5229
5481
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5230
5482
|
const header = {
|
|
@@ -5271,17 +5523,25 @@ var PiSessionStore = class {
|
|
|
5271
5523
|
(e) => e.type !== "session"
|
|
5272
5524
|
);
|
|
5273
5525
|
const fileStat = await fsStat(filepath);
|
|
5526
|
+
const linkedPiFile = extractPiSessionFilePath(fileEntries);
|
|
5527
|
+
const linked = linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath) ? await this.readLinkedPiSession(linkedPiFile) : null;
|
|
5528
|
+
const linkedEntries = linked?.entries.filter(
|
|
5529
|
+
(e) => e.type !== "session"
|
|
5530
|
+
) ?? [];
|
|
5274
5531
|
const uiSnapshot = extractLatestUiSnapshot(fileEntries);
|
|
5275
|
-
const
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5532
|
+
const transcriptEntries = linkedEntries.length > 0 ? linkedEntries : sessionEntries;
|
|
5533
|
+
const messages = uiSnapshot ?? dropEmptyAssistantUiMessages(piMessagesToUIMessages(
|
|
5534
|
+
transcriptEntries.filter((entry) => entry.type === "message").map((entry) => entry.message),
|
|
5535
|
+
sessionId
|
|
5536
|
+
));
|
|
5537
|
+
const title = extractTitle(sessionEntries) ?? extractTitle(linkedEntries) ?? "New session";
|
|
5279
5538
|
const turnCount = messages.filter((m) => m.role === "user").length;
|
|
5539
|
+
const updatedAtMs = Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
|
|
5280
5540
|
return {
|
|
5281
5541
|
id: sessionId,
|
|
5282
5542
|
title,
|
|
5283
5543
|
createdAt: header?.timestamp ?? fileStat.birthtime.toISOString(),
|
|
5284
|
-
updatedAt:
|
|
5544
|
+
updatedAt: new Date(updatedAtMs).toISOString(),
|
|
5285
5545
|
turnCount,
|
|
5286
5546
|
messages
|
|
5287
5547
|
};
|
|
@@ -5292,7 +5552,7 @@ var PiSessionStore = class {
|
|
|
5292
5552
|
type: "ui_snapshot",
|
|
5293
5553
|
id: randomUUID(),
|
|
5294
5554
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5295
|
-
messages
|
|
5555
|
+
messages: sanitizeUiMessages(messages, { dropEmptyAssistantMessages: true })
|
|
5296
5556
|
});
|
|
5297
5557
|
await appendFile(filepath, entry + "\n");
|
|
5298
5558
|
}
|
|
@@ -5356,7 +5616,12 @@ var PiSessionStore = class {
|
|
|
5356
5616
|
const filepath = await this.resolveSessionFile(sessionId).catch(
|
|
5357
5617
|
() => null
|
|
5358
5618
|
);
|
|
5359
|
-
if (filepath)
|
|
5619
|
+
if (!filepath) return;
|
|
5620
|
+
const linkedPiFile = await this.linkedPiFileFor(filepath);
|
|
5621
|
+
await rm4(filepath, { force: true });
|
|
5622
|
+
if (linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath)) {
|
|
5623
|
+
await rm4(linkedPiFile, { force: true });
|
|
5624
|
+
}
|
|
5360
5625
|
}
|
|
5361
5626
|
touchSession(sessionId, title) {
|
|
5362
5627
|
this.resolveSessionFile(sessionId).then((filepath) => {
|
|
@@ -5392,6 +5657,28 @@ var PiSessionStore = class {
|
|
|
5392
5657
|
if (!match) throw new Error(`Session not found: ${sessionId}`);
|
|
5393
5658
|
return join9(this.sessionDir, match);
|
|
5394
5659
|
}
|
|
5660
|
+
async linkedPiFileFor(filepath) {
|
|
5661
|
+
try {
|
|
5662
|
+
const content = await readFile9(filepath, "utf-8");
|
|
5663
|
+
return extractPiSessionFilePath(safeParseEntries(content));
|
|
5664
|
+
} catch {
|
|
5665
|
+
return null;
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
5668
|
+
async referencedPiFiles(filepaths) {
|
|
5669
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
5670
|
+
await Promise.all(filepaths.map(async (filepath) => {
|
|
5671
|
+
try {
|
|
5672
|
+
const content = await readFile9(filepath, "utf-8");
|
|
5673
|
+
const piFilePath = extractPiSessionFilePath(safeParseEntries(content));
|
|
5674
|
+
if (piFilePath && resolve7(piFilePath) !== resolve7(filepath)) {
|
|
5675
|
+
referenced.add(resolve7(piFilePath));
|
|
5676
|
+
}
|
|
5677
|
+
} catch {
|
|
5678
|
+
}
|
|
5679
|
+
}));
|
|
5680
|
+
return referenced;
|
|
5681
|
+
}
|
|
5395
5682
|
async summarizeFile(filepath) {
|
|
5396
5683
|
try {
|
|
5397
5684
|
const [fileStat, content] = await Promise.all([
|
|
@@ -5408,28 +5695,56 @@ var PiSessionStore = class {
|
|
|
5408
5695
|
const sessionEntries = entries.filter(
|
|
5409
5696
|
(e) => e.type !== "session"
|
|
5410
5697
|
);
|
|
5411
|
-
const
|
|
5412
|
-
const
|
|
5698
|
+
const linkedPiFile = extractPiSessionFilePath(entries);
|
|
5699
|
+
const linked = linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath) ? await this.readLinkedPiSession(linkedPiFile) : null;
|
|
5700
|
+
const linkedEntries = linked?.entries.filter(
|
|
5701
|
+
(e) => e.type !== "session"
|
|
5702
|
+
) ?? [];
|
|
5703
|
+
const uiSnapshot = extractLatestUiSnapshot(entries);
|
|
5704
|
+
const title = extractTitle(sessionEntries) ?? extractTitle(linkedEntries) ?? firstUserMessage(linkedEntries) ?? firstUserMessage(sessionEntries) ?? "New session";
|
|
5705
|
+
const turnCount = uiSnapshot ? uiSnapshot.filter((m) => m.role === "user").length : [...sessionEntries, ...linkedEntries].filter(
|
|
5413
5706
|
(e) => e.type === "message" && e.message?.role === "user"
|
|
5414
5707
|
).length;
|
|
5708
|
+
const updatedAtMs = Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
|
|
5415
5709
|
return {
|
|
5416
5710
|
id: header.id,
|
|
5417
5711
|
title,
|
|
5418
5712
|
createdAt: header.timestamp,
|
|
5419
|
-
updatedAt:
|
|
5713
|
+
updatedAt: new Date(updatedAtMs).toISOString(),
|
|
5420
5714
|
turnCount
|
|
5421
5715
|
};
|
|
5422
5716
|
} catch {
|
|
5423
5717
|
return null;
|
|
5424
5718
|
}
|
|
5425
5719
|
}
|
|
5720
|
+
async readLinkedPiSession(filepath) {
|
|
5721
|
+
try {
|
|
5722
|
+
const [fileStat, content] = await Promise.all([
|
|
5723
|
+
fsStat(filepath),
|
|
5724
|
+
readFile9(filepath, "utf-8")
|
|
5725
|
+
]);
|
|
5726
|
+
return { entries: safeParseEntries(content), mtime: fileStat.mtime };
|
|
5727
|
+
} catch {
|
|
5728
|
+
return null;
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5426
5731
|
};
|
|
5732
|
+
function extractPiSessionFilePath(entries) {
|
|
5733
|
+
let piFilePath = null;
|
|
5734
|
+
for (const e of entries) {
|
|
5735
|
+
const rec = e;
|
|
5736
|
+
if (rec.type === "pi_session_file" && typeof rec.path === "string") {
|
|
5737
|
+
piFilePath = rec.path;
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
return piFilePath;
|
|
5741
|
+
}
|
|
5427
5742
|
function extractLatestUiSnapshot(entries) {
|
|
5428
5743
|
let latest = null;
|
|
5429
5744
|
for (const e of entries) {
|
|
5430
5745
|
const rec = e;
|
|
5431
5746
|
if (rec.type === "ui_snapshot" && Array.isArray(rec.messages)) {
|
|
5432
|
-
latest = rec.messages;
|
|
5747
|
+
latest = sanitizeUiMessages(rec.messages, { dropEmptyAssistantMessages: true });
|
|
5433
5748
|
}
|
|
5434
5749
|
}
|
|
5435
5750
|
return latest;
|
|
@@ -5462,7 +5777,10 @@ function safeParseEntries(content) {
|
|
|
5462
5777
|
return results;
|
|
5463
5778
|
}
|
|
5464
5779
|
}
|
|
5465
|
-
function
|
|
5780
|
+
function reconstructedMessageId(sessionId, role, index) {
|
|
5781
|
+
return sessionId ? `${sessionId}-${role}-${index}` : `${role}-${index}`;
|
|
5782
|
+
}
|
|
5783
|
+
function piMessagesToUIMessages(messages, sessionId) {
|
|
5466
5784
|
const result = [];
|
|
5467
5785
|
let currentAssistant = null;
|
|
5468
5786
|
for (const raw of messages) {
|
|
@@ -5474,7 +5792,7 @@ function piMessagesToUIMessages(messages) {
|
|
|
5474
5792
|
const content = msg.content;
|
|
5475
5793
|
const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter((c) => c.type === "text").map((c) => c.text).join("") : "";
|
|
5476
5794
|
result.push({
|
|
5477
|
-
id:
|
|
5795
|
+
id: reconstructedMessageId(sessionId, "user", result.length),
|
|
5478
5796
|
role: "user",
|
|
5479
5797
|
parts: [{ type: "text", text }]
|
|
5480
5798
|
});
|
|
@@ -5508,7 +5826,7 @@ function piMessagesToUIMessages(messages) {
|
|
|
5508
5826
|
}
|
|
5509
5827
|
}
|
|
5510
5828
|
const uiMsg = {
|
|
5511
|
-
id:
|
|
5829
|
+
id: reconstructedMessageId(sessionId, "assistant", result.length),
|
|
5512
5830
|
role: "assistant",
|
|
5513
5831
|
parts
|
|
5514
5832
|
};
|
|
@@ -5546,7 +5864,7 @@ var DEFAULT_POLL_MS = 250;
|
|
|
5546
5864
|
var MAX_PROMPT_CHARS = 1200;
|
|
5547
5865
|
var MAX_TITLE_CHARS = 80;
|
|
5548
5866
|
function sleep(ms) {
|
|
5549
|
-
return new Promise((
|
|
5867
|
+
return new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
5550
5868
|
}
|
|
5551
5869
|
function truncateForPrompt(text) {
|
|
5552
5870
|
const trimmed = text.trim();
|
|
@@ -6021,7 +6339,7 @@ async function applyRequestedSessionOptions(handle, input) {
|
|
|
6021
6339
|
var log3 = createLogger("pi-harness");
|
|
6022
6340
|
var DEFAULT_ATTACHMENT_DIR = "assets/images";
|
|
6023
6341
|
function extForAttachment(filename, contentType) {
|
|
6024
|
-
const fromName =
|
|
6342
|
+
const fromName = extname3(filename).toLowerCase().replace(/^\./, "");
|
|
6025
6343
|
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
6026
6344
|
if (contentType === "image/jpeg") return "jpg";
|
|
6027
6345
|
if (contentType === "image/png") return "png";
|
|
@@ -6181,6 +6499,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6181
6499
|
handle.piSession.dispose();
|
|
6182
6500
|
piSessions.delete(sessionId);
|
|
6183
6501
|
clearNativeFollowUpWork(sessionId);
|
|
6502
|
+
consumedNativeFollowUpKeys.delete(sessionId);
|
|
6184
6503
|
}
|
|
6185
6504
|
const originalDelete = sessionStore.delete.bind(sessionStore);
|
|
6186
6505
|
sessionStore.delete = async (ctx, sessionId) => {
|
|
@@ -6189,6 +6508,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6189
6508
|
};
|
|
6190
6509
|
const nativeFollowUpPending = /* @__PURE__ */ new Set();
|
|
6191
6510
|
const nativeFollowUpQueues = /* @__PURE__ */ new Map();
|
|
6511
|
+
const consumedNativeFollowUpKeys = /* @__PURE__ */ new Map();
|
|
6192
6512
|
function clearNativeFollowUpWork(sessionId) {
|
|
6193
6513
|
nativeFollowUpPending.delete(sessionId);
|
|
6194
6514
|
nativeFollowUpQueues.delete(sessionId);
|
|
@@ -6224,12 +6544,38 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6224
6544
|
}
|
|
6225
6545
|
session._emitQueueUpdate?.();
|
|
6226
6546
|
}
|
|
6547
|
+
function followUpDedupeKeys(options) {
|
|
6548
|
+
const keys = [];
|
|
6549
|
+
if (options?.clientNonce) keys.push(`nonce:${options.clientNonce}`);
|
|
6550
|
+
if (options?.clientSeq !== void 0) keys.push(`seq:${options.clientSeq}`);
|
|
6551
|
+
return keys;
|
|
6552
|
+
}
|
|
6227
6553
|
function hasFollowUpSelector(options) {
|
|
6228
|
-
return
|
|
6554
|
+
return followUpDedupeKeys(options).length > 0;
|
|
6229
6555
|
}
|
|
6230
6556
|
function matchesFollowUpSelector(item, options) {
|
|
6231
6557
|
if (!hasFollowUpSelector(options)) return true;
|
|
6232
|
-
|
|
6558
|
+
const optionKeys = new Set(followUpDedupeKeys(options));
|
|
6559
|
+
return followUpDedupeKeys(item).some((key) => optionKeys.has(key));
|
|
6560
|
+
}
|
|
6561
|
+
function rememberConsumedNativeFollowUp(sessionId, request) {
|
|
6562
|
+
const keys = followUpDedupeKeys(request);
|
|
6563
|
+
if (keys.length === 0) return;
|
|
6564
|
+
const consumed = consumedNativeFollowUpKeys.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
6565
|
+
for (const key of keys) consumed.add(key);
|
|
6566
|
+
consumedNativeFollowUpKeys.set(sessionId, consumed);
|
|
6567
|
+
}
|
|
6568
|
+
function followUpPostDedupeKeys(options) {
|
|
6569
|
+
if (options?.clientNonce) return [`nonce:${options.clientNonce}`];
|
|
6570
|
+
if (options?.clientSeq !== void 0) return [`seq:${options.clientSeq}`];
|
|
6571
|
+
return [];
|
|
6572
|
+
}
|
|
6573
|
+
function hasQueuedOrConsumedNativeFollowUp(sessionId, options) {
|
|
6574
|
+
const optionKeys = new Set(followUpPostDedupeKeys(options));
|
|
6575
|
+
if (optionKeys.size === 0) return false;
|
|
6576
|
+
const consumed = consumedNativeFollowUpKeys.get(sessionId);
|
|
6577
|
+
if (consumed && [...optionKeys].some((key) => consumed.has(key))) return true;
|
|
6578
|
+
return (nativeFollowUpQueues.get(sessionId) ?? []).some((request) => followUpPostDedupeKeys(request).some((key) => optionKeys.has(key)));
|
|
6233
6579
|
}
|
|
6234
6580
|
function removeNativeFollowUp(sessionId, options) {
|
|
6235
6581
|
const queue = nativeFollowUpQueues.get(sessionId);
|
|
@@ -6257,6 +6603,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6257
6603
|
async followUp(sessionId, text, _attachments, displayText = text, options) {
|
|
6258
6604
|
const handle = piSessions.get(sessionId);
|
|
6259
6605
|
if (!handle) throw new Error("followup_session_not_ready");
|
|
6606
|
+
if (hasQueuedOrConsumedNativeFollowUp(sessionId, options)) return;
|
|
6260
6607
|
const queue = nativeFollowUpQueues.get(sessionId) ?? [];
|
|
6261
6608
|
queue.push({
|
|
6262
6609
|
text,
|
|
@@ -6361,34 +6708,34 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6361
6708
|
"tool-output-error"
|
|
6362
6709
|
]);
|
|
6363
6710
|
const standardToolInputsSeen = /* @__PURE__ */ new Set();
|
|
6364
|
-
function isStandardVisibleChunk(
|
|
6365
|
-
const type =
|
|
6711
|
+
function isStandardVisibleChunk(chunk3) {
|
|
6712
|
+
const type = chunk3.type;
|
|
6366
6713
|
return typeof type === "string" && STANDARD_VISIBLE_CHUNK_TYPES.has(type);
|
|
6367
6714
|
}
|
|
6368
6715
|
function namespaceInlinePartIds(input2) {
|
|
6369
6716
|
if (inlineTurnIndex === 0) return input2;
|
|
6370
|
-
return input2.map((
|
|
6371
|
-
const rec =
|
|
6717
|
+
return input2.map((chunk3) => {
|
|
6718
|
+
const rec = chunk3;
|
|
6372
6719
|
if ((rec.type === "text-start" || rec.type === "text-delta" || rec.type === "text-end" || rec.type === "reasoning-start" || rec.type === "reasoning-delta" || rec.type === "reasoning-end") && typeof rec.id === "string") {
|
|
6373
6720
|
return { ...rec, id: `turn-${inlineTurnIndex}:${rec.id}` };
|
|
6374
6721
|
}
|
|
6375
|
-
return
|
|
6722
|
+
return chunk3;
|
|
6376
6723
|
});
|
|
6377
6724
|
}
|
|
6378
6725
|
function filterSdkChunksForCurrentSegment(input2) {
|
|
6379
6726
|
const out = [];
|
|
6380
|
-
for (const
|
|
6381
|
-
const rec =
|
|
6382
|
-
if (inlineTurnIndex > 0 && isStandardVisibleChunk(
|
|
6727
|
+
for (const chunk3 of input2) {
|
|
6728
|
+
const rec = chunk3;
|
|
6729
|
+
if (inlineTurnIndex > 0 && isStandardVisibleChunk(chunk3)) continue;
|
|
6383
6730
|
if (rec.type === "tool-input-available" && rec.toolCallId) {
|
|
6384
6731
|
standardToolInputsSeen.add(rec.toolCallId);
|
|
6385
|
-
out.push(
|
|
6732
|
+
out.push(chunk3);
|
|
6386
6733
|
continue;
|
|
6387
6734
|
}
|
|
6388
6735
|
if ((rec.type === "tool-output-available" || rec.type === "tool-output-error" || rec.type === "tool-output-denied") && rec.toolCallId && !standardToolInputsSeen.has(rec.toolCallId)) {
|
|
6389
6736
|
continue;
|
|
6390
6737
|
}
|
|
6391
|
-
out.push(
|
|
6738
|
+
out.push(chunk3);
|
|
6392
6739
|
}
|
|
6393
6740
|
return out;
|
|
6394
6741
|
}
|
|
@@ -6520,8 +6867,8 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6520
6867
|
const queue = nativeFollowUpQueues.get(input.sessionId);
|
|
6521
6868
|
if (queue?.length) {
|
|
6522
6869
|
const index = queue.findIndex((item) => item.text === text || item.displayText === text);
|
|
6523
|
-
|
|
6524
|
-
|
|
6870
|
+
const consumed = index >= 0 ? queue.splice(index, 1)[0] : queue.shift();
|
|
6871
|
+
rememberConsumedNativeFollowUp(input.sessionId, consumed);
|
|
6525
6872
|
if (queue.length > 0) nativeFollowUpQueues.set(input.sessionId, queue);
|
|
6526
6873
|
else clearNativeFollowUpWork(input.sessionId);
|
|
6527
6874
|
} else {
|
|
@@ -6533,10 +6880,10 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6533
6880
|
} else {
|
|
6534
6881
|
const sdkChunks = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
|
|
6535
6882
|
const shouldBufferTerminalError = event.type === "message_update" && event.assistantMessageEvent?.type === "error";
|
|
6536
|
-
const visibleSdkChunks = shouldBufferTerminalError ? sdkChunks.filter((
|
|
6537
|
-
const type =
|
|
6883
|
+
const visibleSdkChunks = shouldBufferTerminalError ? sdkChunks.filter((chunk3) => {
|
|
6884
|
+
const type = chunk3.type;
|
|
6538
6885
|
if (type === "error" || type === "finish") {
|
|
6539
|
-
pendingTerminalErrorChunks.push(
|
|
6886
|
+
pendingTerminalErrorChunks.push(chunk3);
|
|
6540
6887
|
return false;
|
|
6541
6888
|
}
|
|
6542
6889
|
return true;
|
|
@@ -6544,12 +6891,12 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6544
6891
|
const sdkChunksForTurn = filterSdkChunksForCurrentSegment(visibleSdkChunks);
|
|
6545
6892
|
converted = [...piHistoryChunks, ...sdkChunksForTurn];
|
|
6546
6893
|
}
|
|
6547
|
-
for (const
|
|
6548
|
-
const t =
|
|
6894
|
+
for (const chunk3 of converted) {
|
|
6895
|
+
const t = chunk3.type;
|
|
6549
6896
|
if (t === "text-delta" || t === "data-pi-text-delta" || t === "data-pi-text-end") {
|
|
6550
6897
|
sawTextChunk = true;
|
|
6551
6898
|
}
|
|
6552
|
-
const piEndData =
|
|
6899
|
+
const piEndData = chunk3.data;
|
|
6553
6900
|
if (t === "data-pi-message-end" && piEndData?.role === "assistant" && typeof piEndData.text === "string" && piEndData.text.length > 0) {
|
|
6554
6901
|
sawTextChunk = true;
|
|
6555
6902
|
}
|
|
@@ -6618,7 +6965,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6618
6965
|
const base = basenameForAttachment(a.filename ?? "image");
|
|
6619
6966
|
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
6620
6967
|
const relPath = `${DEFAULT_ATTACHMENT_DIR}/${base}-${unique}.${ext}`;
|
|
6621
|
-
await
|
|
6968
|
+
await mkdir12(join10(opts.cwd, DEFAULT_ATTACHMENT_DIR), { recursive: true });
|
|
6622
6969
|
await writeFile7(join10(opts.cwd, relPath), bytes);
|
|
6623
6970
|
savedPaths.push(relPath);
|
|
6624
6971
|
} catch (err) {
|
|
@@ -6709,7 +7056,7 @@ ${attachmentNotes.join("\n")}` : message,
|
|
|
6709
7056
|
|
|
6710
7057
|
// src/server/harness/pi-coding-agent/pluginLoader.ts
|
|
6711
7058
|
import { readdir as readdir7, stat as stat9, readFile as readFile10 } from "fs/promises";
|
|
6712
|
-
import { join as join11, extname as
|
|
7059
|
+
import { join as join11, extname as extname4, resolve as resolve8, sep as sep6, relative as relative9 } from "path";
|
|
6713
7060
|
import { homedir as homedir4 } from "os";
|
|
6714
7061
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
6715
7062
|
var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
|
|
@@ -6735,7 +7082,7 @@ async function fileExists(path4) {
|
|
|
6735
7082
|
async function discoverFromDir(dir, source) {
|
|
6736
7083
|
if (!await dirExists2(dir)) return [];
|
|
6737
7084
|
const entries = await readdir7(dir);
|
|
6738
|
-
return entries.filter((e) => VALID_EXTENSIONS.has(
|
|
7085
|
+
return entries.filter((e) => VALID_EXTENSIONS.has(extname4(e))).map((e) => ({ path: join11(dir, e), source }));
|
|
6739
7086
|
}
|
|
6740
7087
|
function extractTools(mod) {
|
|
6741
7088
|
const tools = [];
|
|
@@ -6779,8 +7126,8 @@ async function loadNpmPlugin(pkgDir, importFn) {
|
|
|
6779
7126
|
if (!await fileExists(pkgJsonPath)) return [];
|
|
6780
7127
|
const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
|
|
6781
7128
|
const main = pkgJson.main ?? "index.js";
|
|
6782
|
-
const resolvedMain =
|
|
6783
|
-
const resolvedPkgDir =
|
|
7129
|
+
const resolvedMain = resolve8(pkgDir, main);
|
|
7130
|
+
const resolvedPkgDir = resolve8(pkgDir);
|
|
6784
7131
|
if (resolvedMain !== resolvedPkgDir && !resolvedMain.startsWith(resolvedPkgDir + sep6)) {
|
|
6785
7132
|
return [];
|
|
6786
7133
|
}
|
|
@@ -6878,8 +7225,8 @@ function getRuntimeBundleStorageRoot(bundle) {
|
|
|
6878
7225
|
|
|
6879
7226
|
// src/server/tools/operations/bound.ts
|
|
6880
7227
|
import { constants as constants4 } from "fs";
|
|
6881
|
-
import { access as access4, lstat as lstat4, mkdir as
|
|
6882
|
-
import { dirname as dirname11, isAbsolute as isAbsolute7, join as join12, relative as relative10, resolve as
|
|
7228
|
+
import { access as access4, lstat as lstat4, mkdir as mkdir13, readFile as readFile11, readdir as readdir8, readlink, realpath as realpath4, stat as stat10, writeFile as writeFile8 } from "fs/promises";
|
|
7229
|
+
import { dirname as dirname11, isAbsolute as isAbsolute7, join as join12, relative as relative10, resolve as resolve9 } from "path";
|
|
6883
7230
|
function toPosixPath(value) {
|
|
6884
7231
|
return value.split("\\").join("/");
|
|
6885
7232
|
}
|
|
@@ -6935,7 +7282,7 @@ async function walkMatches(root, current, pattern, ignore, limit, out) {
|
|
|
6935
7282
|
const entries = await readdir8(current, { withFileTypes: true });
|
|
6936
7283
|
for (const entry of entries) {
|
|
6937
7284
|
if (out.length >= limit) return;
|
|
6938
|
-
const absolutePath =
|
|
7285
|
+
const absolutePath = resolve9(current, entry.name);
|
|
6939
7286
|
const relativePath = toPosixPath(relative10(root, absolutePath));
|
|
6940
7287
|
if (entry.isDirectory() && shouldSkipDir(relativePath, ignore)) continue;
|
|
6941
7288
|
if (matchesGlob(relativePath, pattern)) {
|
|
@@ -6960,12 +7307,12 @@ async function findNearestExistingAncestor(absPath) {
|
|
|
6960
7307
|
}
|
|
6961
7308
|
}
|
|
6962
7309
|
async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
6963
|
-
const realRoot = await realpath4(
|
|
7310
|
+
const realRoot = await realpath4(resolve9(workspaceRoot));
|
|
6964
7311
|
try {
|
|
6965
7312
|
const s = await lstat4(absPath);
|
|
6966
7313
|
if (s.isSymbolicLink()) {
|
|
6967
7314
|
const target = await readlink(absPath);
|
|
6968
|
-
const resolvedTarget =
|
|
7315
|
+
const resolvedTarget = resolve9(dirname11(absPath), target);
|
|
6969
7316
|
const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
|
|
6970
7317
|
const realAncestor = await realpath4(nearestAncestor);
|
|
6971
7318
|
const rel2 = relative10(realRoot, realAncestor);
|
|
@@ -7037,13 +7384,13 @@ function boundFs(workspaceRoot, opts = {}) {
|
|
|
7037
7384
|
async writeFile(absolutePath, content) {
|
|
7038
7385
|
const storagePath = toStoragePath(absolutePath);
|
|
7039
7386
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7040
|
-
await
|
|
7387
|
+
await mkdir13(dirname11(storagePath), { recursive: true });
|
|
7041
7388
|
await writeFile8(storagePath, content);
|
|
7042
7389
|
},
|
|
7043
7390
|
async mkdir(dir) {
|
|
7044
7391
|
const storagePath = toStoragePath(dir);
|
|
7045
7392
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7046
|
-
await
|
|
7393
|
+
await mkdir13(storagePath, { recursive: true });
|
|
7047
7394
|
}
|
|
7048
7395
|
};
|
|
7049
7396
|
const edit = {
|
|
@@ -7160,8 +7507,8 @@ function vercelBashOps(sandbox, opts = {}) {
|
|
|
7160
7507
|
env: filteredEnv,
|
|
7161
7508
|
signal,
|
|
7162
7509
|
timeoutMs: timeout ? timeout * 1e3 : void 0,
|
|
7163
|
-
onStdout: (
|
|
7164
|
-
onStderr: (
|
|
7510
|
+
onStdout: (chunk3) => onData(Buffer.from(chunk3)),
|
|
7511
|
+
onStderr: (chunk3) => onData(Buffer.from(chunk3))
|
|
7165
7512
|
}).then((result) => ({ exitCode: result.exitCode }));
|
|
7166
7513
|
}
|
|
7167
7514
|
};
|
|
@@ -7328,7 +7675,7 @@ import {
|
|
|
7328
7675
|
truncateHead,
|
|
7329
7676
|
truncateLine
|
|
7330
7677
|
} from "@mariozechner/pi-coding-agent";
|
|
7331
|
-
import { resolve as
|
|
7678
|
+
import { resolve as resolve10, relative as relative12 } from "path";
|
|
7332
7679
|
|
|
7333
7680
|
// src/server/catalog/tools/_shared.ts
|
|
7334
7681
|
function makeError(message) {
|
|
@@ -7480,7 +7827,7 @@ function vercelGrepTool(sandbox, workspaceRoot) {
|
|
|
7480
7827
|
return makeError("pattern is required");
|
|
7481
7828
|
}
|
|
7482
7829
|
if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
|
|
7483
|
-
const resolved =
|
|
7830
|
+
const resolved = resolve10(workspaceRoot, params.path);
|
|
7484
7831
|
const rel = relative12(workspaceRoot, resolved);
|
|
7485
7832
|
if (rel.startsWith("..") || rel.startsWith("/")) {
|
|
7486
7833
|
return makeError(`path "${params.path}" is outside workspace`);
|
|
@@ -7568,6 +7915,84 @@ import {
|
|
|
7568
7915
|
createBashToolDefinition,
|
|
7569
7916
|
createLocalBashOperations
|
|
7570
7917
|
} from "@mariozechner/pi-coding-agent";
|
|
7918
|
+
|
|
7919
|
+
// src/server/catalog/toolReadiness.ts
|
|
7920
|
+
var WORKSPACE_PREPARING_MESSAGE = "Workspace is still preparing. Try again in a moment.";
|
|
7921
|
+
function isRuntimeRequirement(requirement) {
|
|
7922
|
+
return requirement === "runtime-dependencies" || requirement.startsWith("runtime:");
|
|
7923
|
+
}
|
|
7924
|
+
function isReadyState(state) {
|
|
7925
|
+
return state === true || typeof state === "object" && state !== null && state.ready === true;
|
|
7926
|
+
}
|
|
7927
|
+
function blockedState(state) {
|
|
7928
|
+
if (state && typeof state === "object" && state.ready === false) return state;
|
|
7929
|
+
return { ready: false, state: "preparing", retryable: true };
|
|
7930
|
+
}
|
|
7931
|
+
function runtimeRequirementMessage(requirement, state) {
|
|
7932
|
+
if (state === "failed") {
|
|
7933
|
+
return "Runtime setup failed. Retry provisioning or reload the workspace.";
|
|
7934
|
+
}
|
|
7935
|
+
switch (requirement) {
|
|
7936
|
+
case "runtime:python":
|
|
7937
|
+
return "Python runtime dependencies are still installing. This usually takes a few seconds.";
|
|
7938
|
+
case "runtime:node":
|
|
7939
|
+
return "Node runtime dependencies are still installing. This usually takes a few seconds.";
|
|
7940
|
+
default:
|
|
7941
|
+
return "Runtime dependencies are still installing. This usually takes a few seconds.";
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
function workspaceNotReadyToolResult(requirement) {
|
|
7945
|
+
return {
|
|
7946
|
+
content: [{ type: "text", text: WORKSPACE_PREPARING_MESSAGE }],
|
|
7947
|
+
isError: true,
|
|
7948
|
+
details: {
|
|
7949
|
+
code: ErrorCode.enum.WORKSPACE_NOT_READY,
|
|
7950
|
+
retryable: true,
|
|
7951
|
+
requirement
|
|
7952
|
+
}
|
|
7953
|
+
};
|
|
7954
|
+
}
|
|
7955
|
+
function runtimeNotReadyToolResult(requirement, state = { ready: false, state: "preparing", retryable: true }) {
|
|
7956
|
+
const readinessState = state.state ?? "preparing";
|
|
7957
|
+
const code = readinessState === "failed" ? ErrorCode.enum.RUNTIME_PROVISIONING_FAILED : ErrorCode.enum.AGENT_RUNTIME_NOT_READY;
|
|
7958
|
+
const message = state.message ?? runtimeRequirementMessage(requirement, readinessState);
|
|
7959
|
+
return {
|
|
7960
|
+
content: [{ type: "text", text: message }],
|
|
7961
|
+
isError: true,
|
|
7962
|
+
details: {
|
|
7963
|
+
code,
|
|
7964
|
+
retryable: state.retryable ?? true,
|
|
7965
|
+
requirement,
|
|
7966
|
+
state: readinessState,
|
|
7967
|
+
...state.workspaceId ? { workspaceId: state.workspaceId } : {},
|
|
7968
|
+
...state.errorCode ? { errorCode: state.errorCode } : {},
|
|
7969
|
+
...state.causeCode ? { causeCode: state.causeCode } : {}
|
|
7970
|
+
}
|
|
7971
|
+
};
|
|
7972
|
+
}
|
|
7973
|
+
function readinessToolResult(requirement, state) {
|
|
7974
|
+
if (isRuntimeRequirement(requirement)) return runtimeNotReadyToolResult(requirement, blockedState(state));
|
|
7975
|
+
return workspaceNotReadyToolResult(requirement);
|
|
7976
|
+
}
|
|
7977
|
+
function withReadinessRequirements(tool, readinessRequirements) {
|
|
7978
|
+
if (tool.readinessRequirements === readinessRequirements) return tool;
|
|
7979
|
+
return { ...tool, readinessRequirements };
|
|
7980
|
+
}
|
|
7981
|
+
function wrapToolForReadiness(tool, checkReadiness) {
|
|
7982
|
+
if (!checkReadiness || !tool.readinessRequirements || tool.readinessRequirements.length === 0) return tool;
|
|
7983
|
+
return {
|
|
7984
|
+
...tool,
|
|
7985
|
+
async execute(params, ctx) {
|
|
7986
|
+
for (const requirement of tool.readinessRequirements ?? []) {
|
|
7987
|
+
const readiness = checkReadiness(requirement, tool);
|
|
7988
|
+
if (!isReadyState(readiness)) return readinessToolResult(requirement, readiness);
|
|
7989
|
+
}
|
|
7990
|
+
return await tool.execute(params, ctx);
|
|
7991
|
+
}
|
|
7992
|
+
};
|
|
7993
|
+
}
|
|
7994
|
+
|
|
7995
|
+
// src/server/tools/harness/index.ts
|
|
7571
7996
|
function shellEscape2(s) {
|
|
7572
7997
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
7573
7998
|
}
|
|
@@ -7631,27 +8056,77 @@ function bashOptionsForMode(bundle, runtime) {
|
|
|
7631
8056
|
};
|
|
7632
8057
|
}
|
|
7633
8058
|
}
|
|
7634
|
-
function
|
|
8059
|
+
function isRuntimeReady(readiness) {
|
|
8060
|
+
return readiness === void 0 || readiness === true || typeof readiness === "object" && readiness !== null && readiness.ready === true;
|
|
8061
|
+
}
|
|
8062
|
+
function runtimeRequirementForCommand(command) {
|
|
8063
|
+
if (command.includes(".boring-agent/venv/bin/")) return "runtime:python";
|
|
8064
|
+
if (/python(?:3)?\s+-c\s+['\"][^'\"]*(?:from\s+\S+\s+)?import\s+/.test(command)) return "runtime:python";
|
|
8065
|
+
if (command.includes(".boring-agent/node/")) return "runtime:node";
|
|
8066
|
+
if (command.includes(".boring-agent/")) return "runtime-dependencies";
|
|
8067
|
+
return void 0;
|
|
8068
|
+
}
|
|
8069
|
+
function runtimeRequirementForFailure(text) {
|
|
8070
|
+
if (/\.boring-agent\/venv\/bin\/[^\s:]+: (?:No such file or directory|not found)/i.test(text)) return "runtime:python";
|
|
8071
|
+
if (/ModuleNotFoundError: No module named ['\"][^'\"]+['\"]/i.test(text)) return "runtime:python";
|
|
8072
|
+
if (/\.boring-agent\/node\/[^\s:]+: (?:No such file or directory|not found)/i.test(text)) return "runtime:node";
|
|
8073
|
+
if (/(?:^|\n)(?:[^\n:]+:\s*)?(?:line \d+:\s*)?[A-Za-z0-9_.-]+: command not found/i.test(text)) return "runtime-dependencies";
|
|
8074
|
+
return void 0;
|
|
8075
|
+
}
|
|
8076
|
+
function adaptPiTool2(piTool, runtime) {
|
|
7635
8077
|
return {
|
|
7636
8078
|
name: piTool.name,
|
|
7637
8079
|
description: piTool.description,
|
|
7638
8080
|
promptSnippet: piTool.promptSnippet,
|
|
7639
8081
|
parameters: piTool.parameters,
|
|
8082
|
+
readinessRequirements: ["sandbox-exec"],
|
|
7640
8083
|
async execute(params, ctx) {
|
|
7641
|
-
const
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
8084
|
+
const command = typeof params.command === "string" ? params.command : "";
|
|
8085
|
+
const readiness = runtime?.getReadiness?.();
|
|
8086
|
+
const commandRuntimeRequirement = command ? runtimeRequirementForCommand(command) : void 0;
|
|
8087
|
+
if (commandRuntimeRequirement && !isRuntimeReady(readiness)) {
|
|
8088
|
+
return runtimeNotReadyToolResult(
|
|
8089
|
+
commandRuntimeRequirement,
|
|
8090
|
+
readiness && typeof readiness === "object" && readiness.ready === false ? readiness : { ready: false, state: "preparing", retryable: true }
|
|
8091
|
+
);
|
|
8092
|
+
}
|
|
8093
|
+
let result;
|
|
8094
|
+
try {
|
|
8095
|
+
result = await piTool.execute(
|
|
8096
|
+
ctx.toolCallId,
|
|
8097
|
+
params,
|
|
8098
|
+
ctx.abortSignal,
|
|
8099
|
+
ctx.onUpdate ? (update) => {
|
|
8100
|
+
const text2 = update.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
8101
|
+
ctx.onUpdate(text2);
|
|
8102
|
+
} : void 0,
|
|
8103
|
+
{}
|
|
8104
|
+
);
|
|
8105
|
+
} catch (error) {
|
|
8106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8107
|
+
const latestReadiness2 = runtime?.getReadiness?.();
|
|
8108
|
+
const failureRuntimeRequirement2 = runtimeRequirementForFailure(message);
|
|
8109
|
+
if (command && failureRuntimeRequirement2 && !isRuntimeReady(latestReadiness2)) {
|
|
8110
|
+
return runtimeNotReadyToolResult(
|
|
8111
|
+
failureRuntimeRequirement2,
|
|
8112
|
+
latestReadiness2 && typeof latestReadiness2 === "object" && latestReadiness2.ready === false ? latestReadiness2 : { ready: false, state: "preparing", retryable: true }
|
|
8113
|
+
);
|
|
8114
|
+
}
|
|
8115
|
+
throw error;
|
|
8116
|
+
}
|
|
7651
8117
|
const textContent = (result.content ?? []).filter((c) => c.type === "text").map((c) => ({ type: "text", text: c.text }));
|
|
8118
|
+
const text = textContent.map((part) => part.text).join("\n");
|
|
8119
|
+
const latestReadiness = runtime?.getReadiness?.();
|
|
8120
|
+
const failureRuntimeRequirement = runtimeRequirementForFailure(text);
|
|
8121
|
+
if (command && failureRuntimeRequirement && !isRuntimeReady(latestReadiness)) {
|
|
8122
|
+
return runtimeNotReadyToolResult(
|
|
8123
|
+
failureRuntimeRequirement,
|
|
8124
|
+
latestReadiness && typeof latestReadiness === "object" && latestReadiness.ready === false ? latestReadiness : { ready: false, state: "preparing", retryable: true }
|
|
8125
|
+
);
|
|
8126
|
+
}
|
|
7652
8127
|
return {
|
|
7653
8128
|
content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
|
|
7654
|
-
isError:
|
|
8129
|
+
isError: Boolean(result.isError),
|
|
7655
8130
|
details: result.details
|
|
7656
8131
|
};
|
|
7657
8132
|
}
|
|
@@ -7712,7 +8187,7 @@ function createExecuteIsolatedCodeTool(sandbox) {
|
|
|
7712
8187
|
}
|
|
7713
8188
|
function buildHarnessAgentTools(bundle, runtime) {
|
|
7714
8189
|
const tools = [
|
|
7715
|
-
adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle, runtime)))
|
|
8190
|
+
adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle, runtime)), runtime)
|
|
7716
8191
|
];
|
|
7717
8192
|
if (bundle.sandbox.capabilities.includes("isolated-code")) {
|
|
7718
8193
|
tools.push(createExecuteIsolatedCodeTool(bundle.sandbox));
|
|
@@ -8040,9 +8515,9 @@ var TurnBuffer = class {
|
|
|
8040
8515
|
onChunk = /* @__PURE__ */ new Set();
|
|
8041
8516
|
onDone = /* @__PURE__ */ new Set();
|
|
8042
8517
|
gc = null;
|
|
8043
|
-
append(
|
|
8518
|
+
append(chunk3) {
|
|
8044
8519
|
const idx = this.nextIdx++;
|
|
8045
|
-
const entry = { idx, chunk:
|
|
8520
|
+
const entry = { idx, chunk: chunk3 };
|
|
8046
8521
|
this.ring.push(entry);
|
|
8047
8522
|
if (this.ring.length > MAX_CHUNKS) this.ring.shift();
|
|
8048
8523
|
for (const h of this.onChunk) h(entry);
|
|
@@ -8157,11 +8632,11 @@ var VALID_OPS = /* @__PURE__ */ new Set([
|
|
|
8157
8632
|
function isRecord2(value) {
|
|
8158
8633
|
return typeof value === "object" && value !== null;
|
|
8159
8634
|
}
|
|
8160
|
-
function parseFileChangeChunk(
|
|
8161
|
-
if (!isRecord2(
|
|
8635
|
+
function parseFileChangeChunk(chunk3) {
|
|
8636
|
+
if (!isRecord2(chunk3) || chunk3.type !== "data-file-changed") {
|
|
8162
8637
|
return null;
|
|
8163
8638
|
}
|
|
8164
|
-
const data =
|
|
8639
|
+
const data = chunk3.data;
|
|
8165
8640
|
if (!isRecord2(data)) {
|
|
8166
8641
|
return null;
|
|
8167
8642
|
}
|
|
@@ -8187,6 +8662,115 @@ function parseFileChangeChunk(chunk2) {
|
|
|
8187
8662
|
return change;
|
|
8188
8663
|
}
|
|
8189
8664
|
|
|
8665
|
+
// src/server/http/turnManager.ts
|
|
8666
|
+
function chunk2(data) {
|
|
8667
|
+
return data;
|
|
8668
|
+
}
|
|
8669
|
+
var TurnManager = class {
|
|
8670
|
+
buffers = new StreamBufferStore();
|
|
8671
|
+
activeAbortControllersBySession = /* @__PURE__ */ new Map();
|
|
8672
|
+
activeTurnBySession = /* @__PURE__ */ new Map();
|
|
8673
|
+
async startTurn(options) {
|
|
8674
|
+
const reserved = this.reserve(options.sessionId, options.turnId);
|
|
8675
|
+
if (!reserved) return { active: true };
|
|
8676
|
+
try {
|
|
8677
|
+
const runtime = await options.resolveRuntime();
|
|
8678
|
+
options.onSubmitted?.();
|
|
8679
|
+
const chunks = runtime.harness.sendMessage(options.input, {
|
|
8680
|
+
abortSignal: reserved.abortController.signal,
|
|
8681
|
+
workdir: runtime.workdir
|
|
8682
|
+
});
|
|
8683
|
+
this.pump(reserved, chunks, options);
|
|
8684
|
+
return { turnId: reserved.turnId, buffer: reserved.buffer };
|
|
8685
|
+
} catch (err) {
|
|
8686
|
+
this.cleanup(reserved);
|
|
8687
|
+
reserved.buffer.markComplete(() => this.buffers.evict(reserved.sessionId, reserved.turnId));
|
|
8688
|
+
throw err;
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
8691
|
+
abortTurn(sessionId, turnId) {
|
|
8692
|
+
const requestedTurnId = turnId ?? this.activeTurnBySession.get(sessionId);
|
|
8693
|
+
if (!requestedTurnId) return;
|
|
8694
|
+
this.activeAbortControllersBySession.get(sessionId)?.get(requestedTurnId)?.abort();
|
|
8695
|
+
}
|
|
8696
|
+
getActive(sessionId) {
|
|
8697
|
+
return this.buffers.getActive(sessionId);
|
|
8698
|
+
}
|
|
8699
|
+
reserve(sessionId, turnId) {
|
|
8700
|
+
if (this.activeTurnBySession.has(sessionId)) return null;
|
|
8701
|
+
const abortController = new AbortController();
|
|
8702
|
+
const sessionAbortControllers = this.activeAbortControllersBySession.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
8703
|
+
sessionAbortControllers.set(turnId, abortController);
|
|
8704
|
+
this.activeAbortControllersBySession.set(sessionId, sessionAbortControllers);
|
|
8705
|
+
this.activeTurnBySession.set(sessionId, turnId);
|
|
8706
|
+
return {
|
|
8707
|
+
sessionId,
|
|
8708
|
+
turnId,
|
|
8709
|
+
abortController,
|
|
8710
|
+
buffer: this.buffers.create(sessionId, turnId)
|
|
8711
|
+
};
|
|
8712
|
+
}
|
|
8713
|
+
pump(reserved, chunks, options) {
|
|
8714
|
+
void (async () => {
|
|
8715
|
+
let streamFailed = false;
|
|
8716
|
+
try {
|
|
8717
|
+
reserved.buffer.append(chunk2({ type: "data-turn-start", data: { turnId: reserved.turnId } }));
|
|
8718
|
+
for await (const rawChunk of chunks) {
|
|
8719
|
+
const nextChunk = rawChunk;
|
|
8720
|
+
const fileChange = parseFileChangeChunk(nextChunk);
|
|
8721
|
+
if (fileChange) {
|
|
8722
|
+
options.sessionChangesTracker?.record(reserved.sessionId, fileChange);
|
|
8723
|
+
}
|
|
8724
|
+
reserved.buffer.append(nextChunk);
|
|
8725
|
+
}
|
|
8726
|
+
} catch (err) {
|
|
8727
|
+
streamFailed = true;
|
|
8728
|
+
options.onStreamError?.(err);
|
|
8729
|
+
reserved.buffer.append({
|
|
8730
|
+
type: "error",
|
|
8731
|
+
errorText: "internal error"
|
|
8732
|
+
});
|
|
8733
|
+
} finally {
|
|
8734
|
+
this.cleanup(reserved);
|
|
8735
|
+
if (!streamFailed) options.onStreamComplete?.();
|
|
8736
|
+
reserved.buffer.markComplete(() => this.buffers.evict(reserved.sessionId, reserved.turnId));
|
|
8737
|
+
}
|
|
8738
|
+
})();
|
|
8739
|
+
}
|
|
8740
|
+
cleanup(reserved) {
|
|
8741
|
+
const sessionAbortControllers = this.activeAbortControllersBySession.get(reserved.sessionId);
|
|
8742
|
+
if (sessionAbortControllers?.get(reserved.turnId) === reserved.abortController) {
|
|
8743
|
+
sessionAbortControllers.delete(reserved.turnId);
|
|
8744
|
+
if (sessionAbortControllers.size === 0) this.activeAbortControllersBySession.delete(reserved.sessionId);
|
|
8745
|
+
}
|
|
8746
|
+
if (this.activeTurnBySession.get(reserved.sessionId) === reserved.turnId) {
|
|
8747
|
+
this.activeTurnBySession.delete(reserved.sessionId);
|
|
8748
|
+
}
|
|
8749
|
+
}
|
|
8750
|
+
};
|
|
8751
|
+
function createBufferedUiMessageStream(buffer, closeEmitter, cursor = -1) {
|
|
8752
|
+
return createUIMessageStream({
|
|
8753
|
+
async execute({ writer }) {
|
|
8754
|
+
const replayed = buffer.replay(cursor);
|
|
8755
|
+
for (const e of replayed) writer.write(e.chunk);
|
|
8756
|
+
if (buffer.complete) return;
|
|
8757
|
+
await new Promise((resolve11) => {
|
|
8758
|
+
const unsub = buffer.subscribe(
|
|
8759
|
+
(e) => writer.write(e.chunk),
|
|
8760
|
+
() => {
|
|
8761
|
+
unsub();
|
|
8762
|
+
resolve11();
|
|
8763
|
+
}
|
|
8764
|
+
);
|
|
8765
|
+
closeEmitter.on("close", () => {
|
|
8766
|
+
unsub();
|
|
8767
|
+
resolve11();
|
|
8768
|
+
});
|
|
8769
|
+
});
|
|
8770
|
+
}
|
|
8771
|
+
});
|
|
8772
|
+
}
|
|
8773
|
+
|
|
8190
8774
|
// src/server/harness/pi-coding-agent/projectPiDataMessages.ts
|
|
8191
8775
|
function projectPiDataMessages(messages) {
|
|
8192
8776
|
const dataParts = messages.flatMap((message) => message.parts ?? []).filter((part) => {
|
|
@@ -8260,7 +8844,8 @@ var chatBodySchema = z.object({
|
|
|
8260
8844
|
mediaType: z.string().optional(),
|
|
8261
8845
|
url: z.string()
|
|
8262
8846
|
})
|
|
8263
|
-
).max(20).optional()
|
|
8847
|
+
).max(20).optional(),
|
|
8848
|
+
clientTurnId: z.string().min(1).max(128).optional()
|
|
8264
8849
|
});
|
|
8265
8850
|
function addTelemetryProperty(properties, key, value) {
|
|
8266
8851
|
if (typeof value === "string" && value) properties[key] = value;
|
|
@@ -8274,7 +8859,7 @@ function chatRoutes(app, opts, done) {
|
|
|
8274
8859
|
const { sessionChangesTracker } = opts;
|
|
8275
8860
|
const telemetry = opts.telemetry ?? noopTelemetry;
|
|
8276
8861
|
const validateBody = createBodyValidator(chatBodySchema);
|
|
8277
|
-
const
|
|
8862
|
+
const turnManager = new TurnManager();
|
|
8278
8863
|
const lastFollowUpBySession = /* @__PURE__ */ new Map();
|
|
8279
8864
|
const cancelledFollowUpsBySession = /* @__PURE__ */ new Map();
|
|
8280
8865
|
const MAX_FOLLOWUP_CACHE = 5e3;
|
|
@@ -8311,12 +8896,18 @@ function chatRoutes(app, opts, done) {
|
|
|
8311
8896
|
}
|
|
8312
8897
|
throw new Error("chat route requires harness/workdir or getRuntime");
|
|
8313
8898
|
}
|
|
8899
|
+
async function resolveSessionStore(request) {
|
|
8900
|
+
if (opts.getSessionStore) return await opts.getSessionStore(request);
|
|
8901
|
+
if (opts.sessionStore) return opts.sessionStore;
|
|
8902
|
+
const runtime = await resolveRuntime(request);
|
|
8903
|
+
return runtime.harness.sessions;
|
|
8904
|
+
}
|
|
8314
8905
|
app.post(
|
|
8315
8906
|
"/api/v1/agent/chat",
|
|
8316
8907
|
{ preHandler: validateBody },
|
|
8317
8908
|
async (request, reply) => {
|
|
8318
|
-
const { sessionId, message, model, thinkingLevel, attachments } = request.body;
|
|
8319
|
-
const turnId = randomUUID3();
|
|
8909
|
+
const { sessionId, message, model, thinkingLevel, attachments, clientTurnId } = request.body;
|
|
8910
|
+
const turnId = clientTurnId ?? randomUUID3();
|
|
8320
8911
|
const startedAt = Date.now();
|
|
8321
8912
|
const telemetryProperties2 = {
|
|
8322
8913
|
sessionId,
|
|
@@ -8329,83 +8920,57 @@ function chatRoutes(app, opts, done) {
|
|
|
8329
8920
|
name: "agent.chat.started",
|
|
8330
8921
|
properties: telemetryProperties2
|
|
8331
8922
|
});
|
|
8332
|
-
const abortController = new AbortController();
|
|
8333
|
-
let streamStarted = false;
|
|
8334
|
-
let streamCompleted = false;
|
|
8335
|
-
reply.raw.on("close", () => {
|
|
8336
|
-
if (streamStarted && !streamCompleted && !abortController.signal.aborted) {
|
|
8337
|
-
abortController.abort();
|
|
8338
|
-
}
|
|
8339
|
-
});
|
|
8340
|
-
const buf = buffers.create(sessionId, turnId);
|
|
8341
8923
|
try {
|
|
8342
|
-
const
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
sessionChangesTracker?.record(sessionId, fileChange);
|
|
8364
|
-
}
|
|
8365
|
-
buf.append(c);
|
|
8366
|
-
writer.write(c);
|
|
8924
|
+
const startedTurn = await turnManager.startTurn({
|
|
8925
|
+
sessionId,
|
|
8926
|
+
turnId,
|
|
8927
|
+
input: { sessionId, message, model, thinkingLevel, attachments },
|
|
8928
|
+
resolveRuntime: () => resolveRuntime(request),
|
|
8929
|
+
sessionChangesTracker,
|
|
8930
|
+
onSubmitted: () => {
|
|
8931
|
+
safeCapture(telemetry, {
|
|
8932
|
+
name: "agent.chat.message.submitted",
|
|
8933
|
+
properties: telemetryProperties2
|
|
8934
|
+
});
|
|
8935
|
+
},
|
|
8936
|
+
onStreamError: (err) => {
|
|
8937
|
+
request.log.error({ err, sessionId }, "[chat] stream error");
|
|
8938
|
+
safeCapture(telemetry, {
|
|
8939
|
+
name: "agent.chat.failed",
|
|
8940
|
+
properties: {
|
|
8941
|
+
...telemetryProperties2,
|
|
8942
|
+
status: "error",
|
|
8943
|
+
durationMs: Date.now() - startedAt,
|
|
8944
|
+
errorCode: ErrorCode.enum.INTERNAL_ERROR
|
|
8367
8945
|
}
|
|
8368
|
-
}
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
errorCode: ErrorCode.enum.INTERNAL_ERROR
|
|
8378
|
-
}
|
|
8379
|
-
});
|
|
8380
|
-
const errChunk = {
|
|
8381
|
-
type: "error",
|
|
8382
|
-
errorText: "internal error"
|
|
8383
|
-
};
|
|
8384
|
-
buf.append(errChunk);
|
|
8385
|
-
writer.write(errChunk);
|
|
8386
|
-
} finally {
|
|
8387
|
-
streamCompleted = true;
|
|
8388
|
-
if (!streamFailed) {
|
|
8389
|
-
safeCapture(telemetry, {
|
|
8390
|
-
name: "agent.chat.completed",
|
|
8391
|
-
properties: {
|
|
8392
|
-
...telemetryProperties2,
|
|
8393
|
-
status: "ok",
|
|
8394
|
-
durationMs: Date.now() - startedAt
|
|
8395
|
-
}
|
|
8396
|
-
});
|
|
8946
|
+
});
|
|
8947
|
+
},
|
|
8948
|
+
onStreamComplete: () => {
|
|
8949
|
+
safeCapture(telemetry, {
|
|
8950
|
+
name: "agent.chat.completed",
|
|
8951
|
+
properties: {
|
|
8952
|
+
...telemetryProperties2,
|
|
8953
|
+
status: "ok",
|
|
8954
|
+
durationMs: Date.now() - startedAt
|
|
8397
8955
|
}
|
|
8398
|
-
|
|
8399
|
-
}
|
|
8956
|
+
});
|
|
8400
8957
|
}
|
|
8401
8958
|
});
|
|
8402
|
-
|
|
8959
|
+
if ("active" in startedTurn) {
|
|
8960
|
+
return reply.code(409).send({
|
|
8961
|
+
error: {
|
|
8962
|
+
code: ERROR_CODE_CONFLICT,
|
|
8963
|
+
message: "turn_already_active"
|
|
8964
|
+
}
|
|
8965
|
+
});
|
|
8966
|
+
}
|
|
8967
|
+
const stream = createBufferedUiMessageStream(startedTurn.buffer, request.raw);
|
|
8403
8968
|
reply.hijack();
|
|
8404
8969
|
pipeUIMessageStreamToResponse({
|
|
8405
8970
|
response: reply.raw,
|
|
8406
8971
|
stream,
|
|
8407
8972
|
headers: {
|
|
8408
|
-
"X-Turn-Id": turnId,
|
|
8973
|
+
"X-Turn-Id": startedTurn.turnId,
|
|
8409
8974
|
"X-Accel-Buffering": "no",
|
|
8410
8975
|
"Cache-Control": "no-cache, no-transform"
|
|
8411
8976
|
}
|
|
@@ -8423,8 +8988,6 @@ function chatRoutes(app, opts, done) {
|
|
|
8423
8988
|
errorCode: safeTelemetryErrorCode(err?.code)
|
|
8424
8989
|
}
|
|
8425
8990
|
});
|
|
8426
|
-
buf.markComplete(() => buffers.evict(sessionId, turnId));
|
|
8427
|
-
if (streamStarted) return;
|
|
8428
8991
|
const statusCode = err?.statusCode;
|
|
8429
8992
|
const stableCode = err?.code;
|
|
8430
8993
|
if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
|
|
@@ -8456,7 +9019,7 @@ function chatRoutes(app, opts, done) {
|
|
|
8456
9019
|
}
|
|
8457
9020
|
});
|
|
8458
9021
|
}
|
|
8459
|
-
const active =
|
|
9022
|
+
const active = turnManager.getActive(sessionId);
|
|
8460
9023
|
if (!active) {
|
|
8461
9024
|
return reply.code(204).send();
|
|
8462
9025
|
}
|
|
@@ -8473,26 +9036,7 @@ function chatRoutes(app, opts, done) {
|
|
|
8473
9036
|
{ sessionId, cursor, bufHigh: buf.highIdx },
|
|
8474
9037
|
"[resume] replaying"
|
|
8475
9038
|
);
|
|
8476
|
-
const stream =
|
|
8477
|
-
async execute({ writer }) {
|
|
8478
|
-
const replayed = buf.replay(cursor);
|
|
8479
|
-
for (const e of replayed) writer.write(e.chunk);
|
|
8480
|
-
if (buf.complete) return;
|
|
8481
|
-
await new Promise((resolve10) => {
|
|
8482
|
-
const unsub = buf.subscribe(
|
|
8483
|
-
(e) => writer.write(e.chunk),
|
|
8484
|
-
() => {
|
|
8485
|
-
unsub();
|
|
8486
|
-
resolve10();
|
|
8487
|
-
}
|
|
8488
|
-
);
|
|
8489
|
-
request.raw.on("close", () => {
|
|
8490
|
-
unsub();
|
|
8491
|
-
resolve10();
|
|
8492
|
-
});
|
|
8493
|
-
});
|
|
8494
|
-
}
|
|
8495
|
-
});
|
|
9039
|
+
const stream = createBufferedUiMessageStream(buf, request.raw, cursor);
|
|
8496
9040
|
reply.hijack();
|
|
8497
9041
|
pipeUIMessageStreamToResponse({
|
|
8498
9042
|
response: reply.raw,
|
|
@@ -8513,8 +9057,8 @@ function chatRoutes(app, opts, done) {
|
|
|
8513
9057
|
workspaceId: request.workspaceContext?.workspaceId ?? "default"
|
|
8514
9058
|
};
|
|
8515
9059
|
try {
|
|
8516
|
-
const
|
|
8517
|
-
const detail = await
|
|
9060
|
+
const store = await resolveSessionStore(request);
|
|
9061
|
+
const detail = await store.load(ctx, sessionId);
|
|
8518
9062
|
const messages = Array.isArray(detail?.messages) ? detail.messages : [];
|
|
8519
9063
|
return reply.code(200).send({ messages });
|
|
8520
9064
|
} catch {
|
|
@@ -8522,6 +9066,16 @@ function chatRoutes(app, opts, done) {
|
|
|
8522
9066
|
}
|
|
8523
9067
|
}
|
|
8524
9068
|
);
|
|
9069
|
+
app.delete(
|
|
9070
|
+
"/api/v1/agent/chat/:sessionId/turn",
|
|
9071
|
+
async (request, reply) => {
|
|
9072
|
+
const { sessionId } = request.params;
|
|
9073
|
+
const query = request.query;
|
|
9074
|
+
const requestedTurnId = typeof query.turnId === "string" && query.turnId.length > 0 ? query.turnId : void 0;
|
|
9075
|
+
turnManager.abortTurn(sessionId, requestedTurnId);
|
|
9076
|
+
return reply.code(204).send();
|
|
9077
|
+
}
|
|
9078
|
+
);
|
|
8525
9079
|
app.post(
|
|
8526
9080
|
"/api/v1/agent/chat/:sessionId/followup",
|
|
8527
9081
|
async (request, reply) => {
|
|
@@ -8611,9 +9165,9 @@ function chatRoutes(app, opts, done) {
|
|
|
8611
9165
|
workspaceId: request.workspaceContext?.workspaceId ?? "default"
|
|
8612
9166
|
};
|
|
8613
9167
|
try {
|
|
8614
|
-
const
|
|
8615
|
-
if (
|
|
8616
|
-
await
|
|
9168
|
+
const store = await resolveSessionStore(request);
|
|
9169
|
+
if (store.saveMessages) {
|
|
9170
|
+
await store.saveMessages(ctx, sessionId, projectPiDataMessages(body.messages));
|
|
8617
9171
|
}
|
|
8618
9172
|
return reply.code(204).send();
|
|
8619
9173
|
} catch {
|
|
@@ -8626,41 +9180,23 @@ function chatRoutes(app, opts, done) {
|
|
|
8626
9180
|
|
|
8627
9181
|
// src/server/http/routes/models.ts
|
|
8628
9182
|
import { AuthStorage as AuthStorage2, ModelRegistry as ModelRegistry2 } from "@mariozechner/pi-coding-agent";
|
|
8629
|
-
var AUTH_CHECK_TTL_MS = 6e4;
|
|
8630
9183
|
function modelsRoutes(app, _opts, done) {
|
|
8631
9184
|
const authStorage = AuthStorage2.create();
|
|
8632
9185
|
const registry = ModelRegistry2.create(authStorage);
|
|
8633
9186
|
registerConfiguredModelProviders(registry);
|
|
8634
|
-
const providerAuthCache = /* @__PURE__ */ new Map();
|
|
8635
|
-
async function providerHasResolvableAuth(provider) {
|
|
8636
|
-
const now = Date.now();
|
|
8637
|
-
const cached = providerAuthCache.get(provider);
|
|
8638
|
-
if (cached && cached.expiresAt > now) return cached.available;
|
|
8639
|
-
let available = false;
|
|
8640
|
-
try {
|
|
8641
|
-
const model = registry.getAvailable().find((candidate) => candidate.provider === provider);
|
|
8642
|
-
const auth = model ? await registry.getApiKeyAndHeaders(model) : void 0;
|
|
8643
|
-
available = auth?.ok === true && (typeof auth.apiKey === "string" && auth.apiKey.trim().length > 0 || auth.headers !== void 0);
|
|
8644
|
-
} catch {
|
|
8645
|
-
available = false;
|
|
8646
|
-
}
|
|
8647
|
-
providerAuthCache.set(provider, { available, expiresAt: now + AUTH_CHECK_TTL_MS });
|
|
8648
|
-
return available;
|
|
8649
|
-
}
|
|
8650
9187
|
app.get("/api/v1/agent/models", async (_request, reply) => {
|
|
8651
9188
|
const availableModels = registry.getAvailable();
|
|
8652
9189
|
const availableSet = new Set(
|
|
8653
9190
|
availableModels.map((m) => `${m.provider}:${m.id}`)
|
|
8654
9191
|
);
|
|
8655
|
-
const providerAvailability = /* @__PURE__ */ new Map();
|
|
8656
|
-
await Promise.all([...new Set(availableModels.map((m) => m.provider))].map(async (provider) => {
|
|
8657
|
-
providerAvailability.set(provider, await providerHasResolvableAuth(provider));
|
|
8658
|
-
}));
|
|
8659
9192
|
const models = registry.getAll().map((m) => ({
|
|
8660
9193
|
provider: m.provider,
|
|
8661
9194
|
id: m.id,
|
|
8662
9195
|
label: m.label ?? m.id,
|
|
8663
|
-
|
|
9196
|
+
// Keep this endpoint cheap: it is fetched on chat mount, so it must never
|
|
9197
|
+
// block workspace load on deep provider auth resolution. ModelRegistry's
|
|
9198
|
+
// available set is already derived from configured auth sources.
|
|
9199
|
+
available: availableSet.has(`${m.provider}:${m.id}`)
|
|
8664
9200
|
}));
|
|
8665
9201
|
models.sort((a, b) => {
|
|
8666
9202
|
if (a.available !== b.available) return a.available ? -1 : 1;
|
|
@@ -8973,8 +9509,8 @@ function buildAnalysisPrompt(session, transcript, instructions) {
|
|
|
8973
9509
|
].filter(Boolean).join("\n");
|
|
8974
9510
|
}
|
|
8975
9511
|
function analysisTextFromChunks(chunks) {
|
|
8976
|
-
return chunks.map((
|
|
8977
|
-
const record =
|
|
9512
|
+
return chunks.map((chunk3) => {
|
|
9513
|
+
const record = chunk3;
|
|
8978
9514
|
return record.type === "text-delta" && typeof record.delta === "string" ? record.delta : "";
|
|
8979
9515
|
}).join("").trim();
|
|
8980
9516
|
}
|
|
@@ -9104,14 +9640,14 @@ function sessionRoutes(app, opts, done) {
|
|
|
9104
9640
|
}
|
|
9105
9641
|
const abortController = new AbortController();
|
|
9106
9642
|
const chunks = [];
|
|
9107
|
-
for await (const
|
|
9643
|
+
for await (const chunk3 of runtime.harness.sendMessage(
|
|
9108
9644
|
{ sessionId: analysisSession.id, message: prompt },
|
|
9109
9645
|
{
|
|
9110
9646
|
abortSignal: abortController.signal,
|
|
9111
9647
|
workdir: runtime.workdir
|
|
9112
9648
|
}
|
|
9113
9649
|
)) {
|
|
9114
|
-
chunks.push(
|
|
9650
|
+
chunks.push(chunk3);
|
|
9115
9651
|
}
|
|
9116
9652
|
return {
|
|
9117
9653
|
sourceSession: {
|
|
@@ -9241,12 +9777,35 @@ function readyStatusRoutes(app, opts, done) {
|
|
|
9241
9777
|
app.get("/api/v1/ready-status", async (request, reply) => {
|
|
9242
9778
|
const tracker = opts.getTracker ? await opts.getTracker(request) : opts.tracker;
|
|
9243
9779
|
if (!tracker) throw new Error("ready-status route requires tracker or getTracker");
|
|
9780
|
+
const initial = tracker.getReadiness();
|
|
9781
|
+
const initialEvent = {
|
|
9782
|
+
state: tracker.state,
|
|
9783
|
+
sandboxReady: initial.sandboxReady,
|
|
9784
|
+
harnessReady: initial.harnessReady,
|
|
9785
|
+
capabilities: initial.capabilities,
|
|
9786
|
+
message: initial.degradedReason,
|
|
9787
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9788
|
+
};
|
|
9789
|
+
const initialRuntimePending = initialEvent.capabilities.runtimeDependencies.state === "preparing";
|
|
9790
|
+
if (initialEvent.state === "degraded" || initialEvent.state === "ready" && !initialRuntimePending) {
|
|
9791
|
+
return reply.type("text/event-stream").headers({ "Cache-Control": "no-cache" }).send(`:
|
|
9792
|
+
|
|
9793
|
+
event: status
|
|
9794
|
+
data: ${JSON.stringify(initialEvent)}
|
|
9795
|
+
|
|
9796
|
+
`);
|
|
9797
|
+
}
|
|
9244
9798
|
reply.raw.writeHead(200, {
|
|
9245
9799
|
"Content-Type": "text/event-stream",
|
|
9246
9800
|
"Cache-Control": "no-cache",
|
|
9247
9801
|
Connection: "keep-alive"
|
|
9248
9802
|
});
|
|
9249
|
-
reply.raw.write(
|
|
9803
|
+
reply.raw.write(`:
|
|
9804
|
+
|
|
9805
|
+
event: status
|
|
9806
|
+
data: ${JSON.stringify(initialEvent)}
|
|
9807
|
+
|
|
9808
|
+
`);
|
|
9250
9809
|
let closed = false;
|
|
9251
9810
|
let unsubscribe = null;
|
|
9252
9811
|
const closeStream = () => {
|
|
@@ -9255,16 +9814,19 @@ function readyStatusRoutes(app, opts, done) {
|
|
|
9255
9814
|
unsubscribe?.();
|
|
9256
9815
|
reply.raw.end();
|
|
9257
9816
|
};
|
|
9817
|
+
request.raw.on("close", closeStream);
|
|
9818
|
+
reply.hijack();
|
|
9258
9819
|
unsubscribe = tracker.subscribe((event) => {
|
|
9259
9820
|
if (closed) return;
|
|
9260
9821
|
reply.raw.write(`event: status
|
|
9261
9822
|
data: ${JSON.stringify(event)}
|
|
9262
9823
|
|
|
9263
9824
|
`);
|
|
9264
|
-
|
|
9825
|
+
const runtimePending = event.capabilities.runtimeDependencies.state === "preparing";
|
|
9826
|
+
if (event.state === "degraded" || event.state === "ready" && !runtimePending) {
|
|
9827
|
+
queueMicrotask(closeStream);
|
|
9828
|
+
}
|
|
9265
9829
|
});
|
|
9266
|
-
request.raw.on("close", closeStream);
|
|
9267
|
-
reply.hijack();
|
|
9268
9830
|
return reply;
|
|
9269
9831
|
});
|
|
9270
9832
|
done();
|
|
@@ -9362,14 +9924,26 @@ function searchRoutes(app, opts, done) {
|
|
|
9362
9924
|
}
|
|
9363
9925
|
|
|
9364
9926
|
// src/server/sandbox/vercel-sandbox/readyStatus.ts
|
|
9927
|
+
function defaultCapabilities(sandboxReady, harnessReady) {
|
|
9928
|
+
return {
|
|
9929
|
+
chat: { state: harnessReady ? "ready" : "preparing" },
|
|
9930
|
+
workspace: { state: sandboxReady ? "ready" : "preparing" },
|
|
9931
|
+
runtimeDependencies: { state: "ready" }
|
|
9932
|
+
};
|
|
9933
|
+
}
|
|
9365
9934
|
var ReadyStatusTracker = class {
|
|
9366
9935
|
_sandboxReady;
|
|
9367
9936
|
_harnessReady;
|
|
9368
9937
|
_degradedReason;
|
|
9938
|
+
_capabilities;
|
|
9369
9939
|
subscribers = /* @__PURE__ */ new Set();
|
|
9370
9940
|
constructor(opts) {
|
|
9371
9941
|
this._sandboxReady = opts?.sandboxReady ?? false;
|
|
9372
9942
|
this._harnessReady = opts?.harnessReady ?? false;
|
|
9943
|
+
this._capabilities = {
|
|
9944
|
+
...defaultCapabilities(this._sandboxReady, this._harnessReady),
|
|
9945
|
+
...opts?.capabilities ?? {}
|
|
9946
|
+
};
|
|
9373
9947
|
}
|
|
9374
9948
|
get state() {
|
|
9375
9949
|
if (this._degradedReason) return "degraded";
|
|
@@ -9382,23 +9956,52 @@ var ReadyStatusTracker = class {
|
|
|
9382
9956
|
return {
|
|
9383
9957
|
sandboxReady: this._sandboxReady,
|
|
9384
9958
|
harnessReady: this._harnessReady,
|
|
9959
|
+
capabilities: this.getCapabilities(),
|
|
9385
9960
|
degradedReason: this._degradedReason
|
|
9386
9961
|
};
|
|
9387
9962
|
}
|
|
9963
|
+
getCapabilities() {
|
|
9964
|
+
return {
|
|
9965
|
+
chat: { ...this._capabilities.chat },
|
|
9966
|
+
workspace: { ...this._capabilities.workspace },
|
|
9967
|
+
runtimeDependencies: { ...this._capabilities.runtimeDependencies }
|
|
9968
|
+
};
|
|
9969
|
+
}
|
|
9970
|
+
updateCapability(name, detail) {
|
|
9971
|
+
this._capabilities = {
|
|
9972
|
+
...this._capabilities,
|
|
9973
|
+
[name]: { ...detail }
|
|
9974
|
+
};
|
|
9975
|
+
this.emit();
|
|
9976
|
+
}
|
|
9977
|
+
updateRuntimeDependencies(detail) {
|
|
9978
|
+
this.updateCapability("runtimeDependencies", detail);
|
|
9979
|
+
}
|
|
9388
9980
|
markSandboxReady() {
|
|
9389
9981
|
if (this._sandboxReady) return;
|
|
9390
9982
|
this._sandboxReady = true;
|
|
9983
|
+
if (this._capabilities.workspace.state === "preparing") {
|
|
9984
|
+
this._capabilities.workspace = { state: "ready" };
|
|
9985
|
+
}
|
|
9391
9986
|
this.emit();
|
|
9392
9987
|
}
|
|
9393
9988
|
markHarnessReady() {
|
|
9394
9989
|
if (this._harnessReady) return;
|
|
9395
9990
|
this._harnessReady = true;
|
|
9991
|
+
if (this._capabilities.chat.state === "preparing") {
|
|
9992
|
+
this._capabilities.chat = { state: "ready" };
|
|
9993
|
+
}
|
|
9396
9994
|
this.emit();
|
|
9397
9995
|
}
|
|
9398
9996
|
markDegraded(reason) {
|
|
9399
9997
|
this._degradedReason = reason;
|
|
9400
9998
|
this.emit();
|
|
9401
9999
|
}
|
|
10000
|
+
clearDegraded() {
|
|
10001
|
+
if (!this._degradedReason) return;
|
|
10002
|
+
this._degradedReason = void 0;
|
|
10003
|
+
this.emit();
|
|
10004
|
+
}
|
|
9402
10005
|
subscribe(handler) {
|
|
9403
10006
|
this.subscribers.add(handler);
|
|
9404
10007
|
handler(this.snapshot());
|
|
@@ -9415,6 +10018,7 @@ var ReadyStatusTracker = class {
|
|
|
9415
10018
|
state: this.state,
|
|
9416
10019
|
sandboxReady: this._sandboxReady,
|
|
9417
10020
|
harnessReady: this._harnessReady,
|
|
10021
|
+
capabilities: this.getCapabilities(),
|
|
9418
10022
|
message: this._degradedReason,
|
|
9419
10023
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9420
10024
|
};
|
|
@@ -9510,6 +10114,7 @@ async function createAgentApp(opts = {}) {
|
|
|
9510
10114
|
await app.register(chatRoutes, {
|
|
9511
10115
|
harness,
|
|
9512
10116
|
workdir: runtimeBundle.workspace.root,
|
|
10117
|
+
sessionStore: harness.sessions,
|
|
9513
10118
|
sessionChangesTracker,
|
|
9514
10119
|
telemetry: opts.telemetry
|
|
9515
10120
|
});
|
|
@@ -9570,36 +10175,6 @@ function applyCspHeaders(response, opts = {}) {
|
|
|
9570
10175
|
import { basename as basename3 } from "path";
|
|
9571
10176
|
import { AuthStorage as AuthStorage3, ModelRegistry as ModelRegistry3 } from "@mariozechner/pi-coding-agent";
|
|
9572
10177
|
|
|
9573
|
-
// src/server/catalog/toolReadiness.ts
|
|
9574
|
-
var WORKSPACE_PREPARING_MESSAGE = "Workspace is still preparing. Try again in a moment.";
|
|
9575
|
-
function workspaceNotReadyToolResult(requirement) {
|
|
9576
|
-
return {
|
|
9577
|
-
content: [{ type: "text", text: WORKSPACE_PREPARING_MESSAGE }],
|
|
9578
|
-
isError: true,
|
|
9579
|
-
details: {
|
|
9580
|
-
code: ErrorCode.enum.WORKSPACE_NOT_READY,
|
|
9581
|
-
retryable: true,
|
|
9582
|
-
requirement
|
|
9583
|
-
}
|
|
9584
|
-
};
|
|
9585
|
-
}
|
|
9586
|
-
function withReadinessRequirements(tool, readinessRequirements) {
|
|
9587
|
-
if (tool.readinessRequirements === readinessRequirements) return tool;
|
|
9588
|
-
return { ...tool, readinessRequirements };
|
|
9589
|
-
}
|
|
9590
|
-
function wrapToolForReadiness(tool, checkReadiness) {
|
|
9591
|
-
if (!checkReadiness || !tool.readinessRequirements || tool.readinessRequirements.length === 0) return tool;
|
|
9592
|
-
return {
|
|
9593
|
-
...tool,
|
|
9594
|
-
async execute(params, ctx) {
|
|
9595
|
-
for (const requirement of tool.readinessRequirements ?? []) {
|
|
9596
|
-
if (!checkReadiness(requirement, tool)) return workspaceNotReadyToolResult(requirement);
|
|
9597
|
-
}
|
|
9598
|
-
return await tool.execute(params, ctx);
|
|
9599
|
-
}
|
|
9600
|
-
};
|
|
9601
|
-
}
|
|
9602
|
-
|
|
9603
10178
|
// src/server/catalog/mergeTools.ts
|
|
9604
10179
|
function setLastRegistered(merged, tool) {
|
|
9605
10180
|
merged.delete(tool.name);
|
|
@@ -9634,11 +10209,11 @@ function mergeTools(options) {
|
|
|
9634
10209
|
|
|
9635
10210
|
// src/server/tools/upload/index.ts
|
|
9636
10211
|
import { readFile as readFile12 } from "fs/promises";
|
|
9637
|
-
import { extname as
|
|
10212
|
+
import { extname as extname5, join as join13 } from "path";
|
|
9638
10213
|
var DEFAULT_UPLOAD_DIR = "assets/images";
|
|
9639
10214
|
var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
|
|
9640
10215
|
function contentTypeFromExt(path4) {
|
|
9641
|
-
switch (
|
|
10216
|
+
switch (extname5(path4).toLowerCase()) {
|
|
9642
10217
|
case ".jpg":
|
|
9643
10218
|
case ".jpeg":
|
|
9644
10219
|
return "image/jpeg";
|
|
@@ -9657,7 +10232,7 @@ function contentTypeFromExt(path4) {
|
|
|
9657
10232
|
}
|
|
9658
10233
|
}
|
|
9659
10234
|
function extForUpload2(filename, contentType) {
|
|
9660
|
-
const fromName =
|
|
10235
|
+
const fromName = extname5(filename).toLowerCase().replace(/^\./, "");
|
|
9661
10236
|
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
9662
10237
|
if (contentType === "image/jpeg") return "jpg";
|
|
9663
10238
|
if (contentType === "image/png") return "png";
|
|
@@ -9823,6 +10398,29 @@ function createRuntimeProvisioningFailedError(workspaceId, cause) {
|
|
|
9823
10398
|
}
|
|
9824
10399
|
);
|
|
9825
10400
|
}
|
|
10401
|
+
function isRuntimeReadinessRequirement(requirement) {
|
|
10402
|
+
return requirement === "runtime-dependencies" || requirement.startsWith("runtime:");
|
|
10403
|
+
}
|
|
10404
|
+
function causeCodeFrom(error) {
|
|
10405
|
+
const code = error?.code;
|
|
10406
|
+
return typeof code === "string" ? code : void 0;
|
|
10407
|
+
}
|
|
10408
|
+
function createRuntimeReadinessCheck(workspaceId, getRuntimeDependencies) {
|
|
10409
|
+
return (requirement) => {
|
|
10410
|
+
if (!isRuntimeReadinessRequirement(requirement)) return true;
|
|
10411
|
+
const runtimeDependencies = getRuntimeDependencies();
|
|
10412
|
+
if (runtimeDependencies.state === "ready" || runtimeDependencies.state === "not-started") return true;
|
|
10413
|
+
return {
|
|
10414
|
+
ready: false,
|
|
10415
|
+
state: runtimeDependencies.state,
|
|
10416
|
+
errorCode: runtimeDependencies.errorCode,
|
|
10417
|
+
causeCode: runtimeDependencies.causeCode,
|
|
10418
|
+
message: runtimeDependencies.message,
|
|
10419
|
+
workspaceId,
|
|
10420
|
+
retryable: runtimeDependencies.retryable ?? true
|
|
10421
|
+
};
|
|
10422
|
+
};
|
|
10423
|
+
}
|
|
9826
10424
|
var registerAgentRoutes = async (app, opts) => {
|
|
9827
10425
|
const workspaceRoot = opts.workspaceRoot ?? process.cwd();
|
|
9828
10426
|
const sessionId = opts.sessionId ?? DEFAULT_WORKSPACE_ID3;
|
|
@@ -9918,14 +10516,99 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9918
10516
|
requestId: request?.id,
|
|
9919
10517
|
telemetry: opts.telemetry
|
|
9920
10518
|
};
|
|
9921
|
-
let runtimeProvisioning
|
|
10519
|
+
let runtimeProvisioning;
|
|
10520
|
+
let runtimeDependencies = hasRuntimeProvisioningInput ? {
|
|
10521
|
+
state: "preparing",
|
|
10522
|
+
requirement: "runtime-dependencies",
|
|
10523
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10524
|
+
retryable: true
|
|
10525
|
+
} : { state: "ready" };
|
|
10526
|
+
let provisioningGeneration = 0;
|
|
9922
10527
|
const runtimeBundle = await modeAdapter.create(modeCtx);
|
|
10528
|
+
const readyTracker = new ReadyStatusTracker({
|
|
10529
|
+
sandboxReady: resolvedMode !== "vercel-sandbox",
|
|
10530
|
+
harnessReady: false,
|
|
10531
|
+
capabilities: {
|
|
10532
|
+
chat: { state: "preparing" },
|
|
10533
|
+
workspace: { state: resolvedMode !== "vercel-sandbox" ? "ready" : "preparing" },
|
|
10534
|
+
runtimeDependencies
|
|
10535
|
+
}
|
|
10536
|
+
});
|
|
10537
|
+
if (resolvedMode === "vercel-sandbox") {
|
|
10538
|
+
queueMicrotask(() => readyTracker.markSandboxReady());
|
|
10539
|
+
}
|
|
10540
|
+
let binding;
|
|
10541
|
+
const updateRuntimeDependencies = (next) => {
|
|
10542
|
+
runtimeDependencies = next;
|
|
10543
|
+
if (binding) binding.runtimeDependencies = next;
|
|
10544
|
+
readyTracker.updateRuntimeDependencies(next);
|
|
10545
|
+
};
|
|
10546
|
+
const startRuntimeProvisioning = (provisionRequest) => {
|
|
10547
|
+
if (!hasRuntimeProvisioningInput) return void 0;
|
|
10548
|
+
if (binding?.runtimeProvisioningTask && runtimeDependencies.state === "preparing") {
|
|
10549
|
+
return binding.runtimeProvisioningTask;
|
|
10550
|
+
}
|
|
10551
|
+
const generation = ++provisioningGeneration;
|
|
10552
|
+
readyTracker.clearDegraded();
|
|
10553
|
+
updateRuntimeDependencies({
|
|
10554
|
+
state: "preparing",
|
|
10555
|
+
requirement: "runtime-dependencies",
|
|
10556
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10557
|
+
retryable: true
|
|
10558
|
+
});
|
|
10559
|
+
const task = runRuntimeProvisioning(workspaceId, scope, provisionRequest).then(
|
|
10560
|
+
async (result) => {
|
|
10561
|
+
if (generation !== provisioningGeneration) return result;
|
|
10562
|
+
runtimeProvisioning = result;
|
|
10563
|
+
if (binding) binding.runtimeProvisioning = result;
|
|
10564
|
+
if (binding?.harness.reloadSession) {
|
|
10565
|
+
try {
|
|
10566
|
+
const sessions = await binding.harness.sessions.list({ workspaceId });
|
|
10567
|
+
await Promise.allSettled(
|
|
10568
|
+
sessions.map((session) => binding?.harness.reloadSession?.(session.id))
|
|
10569
|
+
);
|
|
10570
|
+
} catch (error) {
|
|
10571
|
+
app.log.warn({ err: error, workspaceId }, "[agent] failed to refresh harness sessions after runtime provisioning");
|
|
10572
|
+
}
|
|
10573
|
+
}
|
|
10574
|
+
updateRuntimeDependencies({
|
|
10575
|
+
state: "ready",
|
|
10576
|
+
requirement: "runtime-dependencies",
|
|
10577
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10578
|
+
retryable: true
|
|
10579
|
+
});
|
|
10580
|
+
return result;
|
|
10581
|
+
},
|
|
10582
|
+
(error) => {
|
|
10583
|
+
if (generation !== provisioningGeneration) throw error;
|
|
10584
|
+
const causeCode = causeCodeFrom(error);
|
|
10585
|
+
updateRuntimeDependencies({
|
|
10586
|
+
state: "failed",
|
|
10587
|
+
requirement: "runtime-dependencies",
|
|
10588
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10589
|
+
errorCode: ErrorCode.enum.RUNTIME_PROVISIONING_FAILED,
|
|
10590
|
+
...causeCode ? { causeCode } : {},
|
|
10591
|
+
retryable: true,
|
|
10592
|
+
message: "Agent runtime provisioning failed. Reload the workspace and try again."
|
|
10593
|
+
});
|
|
10594
|
+
readyTracker.markDegraded("runtime dependency provisioning failed");
|
|
10595
|
+
app.log.warn({ err: error, workspaceId }, "[agent] background runtime provisioning failed");
|
|
10596
|
+
throw error;
|
|
10597
|
+
}
|
|
10598
|
+
);
|
|
10599
|
+
task.catch(() => {
|
|
10600
|
+
});
|
|
10601
|
+
if (binding) binding.runtimeProvisioningTask = task;
|
|
10602
|
+
return task;
|
|
10603
|
+
};
|
|
10604
|
+
const checkReadiness = createRuntimeReadinessCheck(workspaceId, () => runtimeDependencies);
|
|
9923
10605
|
const standardTools = [
|
|
9924
10606
|
...buildHarnessAgentTools(runtimeBundle, {
|
|
9925
10607
|
getCurrent: () => runtimeProvisioning ? {
|
|
9926
10608
|
env: runtimeProvisioning.env,
|
|
9927
10609
|
pathEntries: runtimeProvisioning.pathEntries
|
|
9928
|
-
} : void 0
|
|
10610
|
+
} : void 0,
|
|
10611
|
+
getReadiness: () => checkReadiness("runtime:python", {})
|
|
9929
10612
|
}),
|
|
9930
10613
|
...buildFilesystemAgentTools(runtimeBundle),
|
|
9931
10614
|
...buildUploadAgentTools(runtimeBundle)
|
|
@@ -9958,7 +10641,8 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9958
10641
|
...scopedExtraTools
|
|
9959
10642
|
],
|
|
9960
10643
|
pluginTools,
|
|
9961
|
-
logger: app.log
|
|
10644
|
+
logger: app.log,
|
|
10645
|
+
checkReadiness
|
|
9962
10646
|
});
|
|
9963
10647
|
const harnessFactory = opts.harnessFactory ?? ((input) => createPiCodingAgentHarness({
|
|
9964
10648
|
...input,
|
|
@@ -9967,9 +10651,18 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9967
10651
|
noSkills: true,
|
|
9968
10652
|
...scope.pi,
|
|
9969
10653
|
additionalSkillPaths: [
|
|
9970
|
-
...runtimeProvisioning?.skillPaths ?? [],
|
|
9971
10654
|
...scope.pi?.additionalSkillPaths ?? []
|
|
9972
|
-
]
|
|
10655
|
+
],
|
|
10656
|
+
getHotReloadableResources: () => {
|
|
10657
|
+
const hot = scope.pi?.getHotReloadableResources?.() ?? {};
|
|
10658
|
+
return {
|
|
10659
|
+
...hot,
|
|
10660
|
+
additionalSkillPaths: [
|
|
10661
|
+
...runtimeProvisioning?.skillPaths ?? [],
|
|
10662
|
+
...hot.additionalSkillPaths ?? []
|
|
10663
|
+
]
|
|
10664
|
+
};
|
|
10665
|
+
}
|
|
9973
10666
|
}
|
|
9974
10667
|
}));
|
|
9975
10668
|
const harness = await harnessFactory({
|
|
@@ -9980,24 +10673,22 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9980
10673
|
systemPromptDynamic: opts.getSystemPromptDynamic ? () => opts.getSystemPromptDynamic?.({ workspaceId, workspaceRoot: root }) : opts.systemPromptDynamic,
|
|
9981
10674
|
telemetry: opts.telemetry
|
|
9982
10675
|
});
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
harnessReady: true
|
|
9986
|
-
});
|
|
9987
|
-
if (resolvedMode === "vercel-sandbox") {
|
|
9988
|
-
queueMicrotask(() => readyTracker.markSandboxReady());
|
|
9989
|
-
}
|
|
9990
|
-
return {
|
|
10676
|
+
readyTracker.markHarnessReady();
|
|
10677
|
+
binding = {
|
|
9991
10678
|
runtimeBundle,
|
|
9992
10679
|
runtimeProvisioning,
|
|
10680
|
+
runtimeDependencies,
|
|
10681
|
+
runtimeProvisioningTask: void 0,
|
|
9993
10682
|
reprovision: async (reloadRequest) => {
|
|
9994
|
-
|
|
9995
|
-
return
|
|
10683
|
+
const result = await startRuntimeProvisioning(reloadRequest);
|
|
10684
|
+
return await result;
|
|
9996
10685
|
},
|
|
9997
10686
|
harness,
|
|
9998
10687
|
tools,
|
|
9999
10688
|
readyTracker
|
|
10000
10689
|
};
|
|
10690
|
+
startRuntimeProvisioning(request);
|
|
10691
|
+
return binding;
|
|
10001
10692
|
}
|
|
10002
10693
|
function createRuntimeBindingEntry(workspaceId, scope, request) {
|
|
10003
10694
|
const entry = {
|
|
@@ -10091,6 +10782,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
10091
10782
|
const hasRuntimeProvisioningInput = opts.provisionWorkspace !== false && Boolean(opts.provisionRuntime);
|
|
10092
10783
|
const staticBinding = requestScopedRuntime ? null : await getOrCreateRuntimeBinding(sessionId);
|
|
10093
10784
|
const skillsScopeByRequest = /* @__PURE__ */ new WeakMap();
|
|
10785
|
+
const earlySessionStores = /* @__PURE__ */ new Map();
|
|
10094
10786
|
function getSkillsScopeForRequest(request) {
|
|
10095
10787
|
let promise = skillsScopeByRequest.get(request);
|
|
10096
10788
|
if (!promise) {
|
|
@@ -10103,6 +10795,18 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
10103
10795
|
if (staticBinding) return staticBinding;
|
|
10104
10796
|
return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request, options);
|
|
10105
10797
|
}
|
|
10798
|
+
async function getSessionStoreForRequest(request) {
|
|
10799
|
+
if (staticBinding) return staticBinding.harness.sessions;
|
|
10800
|
+
const scope = await resolveRuntimeScope(getRequestWorkspaceId(request), request);
|
|
10801
|
+
const cached = earlySessionStores.get(scope.key);
|
|
10802
|
+
if (cached) return cached;
|
|
10803
|
+
const store = new PiSessionStore(scope.root, {
|
|
10804
|
+
sessionNamespace: scope.sessionNamespace,
|
|
10805
|
+
storageCwd: scope.root
|
|
10806
|
+
});
|
|
10807
|
+
earlySessionStores.set(scope.key, store);
|
|
10808
|
+
return store;
|
|
10809
|
+
}
|
|
10106
10810
|
const agentToolNames = staticBinding ? staticBinding.tools.map((tool) => tool.name) : [
|
|
10107
10811
|
...STANDARD_AGENT_TOOL_NAMES,
|
|
10108
10812
|
...(opts.extraTools ?? []).map((tool) => tool.name)
|
|
@@ -10171,20 +10875,18 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
10171
10875
|
});
|
|
10172
10876
|
await app.register(chatRoutes, {
|
|
10173
10877
|
getRuntime: async (request) => {
|
|
10174
|
-
const binding = await getBindingForRequest(request
|
|
10878
|
+
const binding = await getBindingForRequest(request);
|
|
10175
10879
|
return {
|
|
10176
10880
|
harness: binding.harness,
|
|
10177
10881
|
workdir: binding.runtimeBundle.workspace.root
|
|
10178
10882
|
};
|
|
10179
10883
|
},
|
|
10884
|
+
getSessionStore: getSessionStoreForRequest,
|
|
10180
10885
|
sessionChangesTracker,
|
|
10181
10886
|
telemetry: opts.telemetry
|
|
10182
10887
|
});
|
|
10183
10888
|
await app.register(sessionRoutes, {
|
|
10184
|
-
getSessionStore:
|
|
10185
|
-
const binding = await getBindingForRequest(request);
|
|
10186
|
-
return binding.harness.sessions;
|
|
10187
|
-
},
|
|
10889
|
+
getSessionStore: getSessionStoreForRequest,
|
|
10188
10890
|
getRuntime: async (request) => {
|
|
10189
10891
|
const binding = await getBindingForRequest(request);
|
|
10190
10892
|
return {
|