@hachej/boring-agent 0.1.31 → 0.1.33
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 +50 -3
- package/dist/front/index.js +774 -307
- package/dist/front/styles.css +40 -23
- package/dist/{harness-CQ0uw6xW.d.ts → harness-dYuMzxmp.d.ts} +6 -2
- package/dist/server/index.d.ts +18 -12
- package/dist/server/index.js +1161 -414
- 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) {
|
|
@@ -3346,11 +3620,11 @@ async function ensurePythonRuntime(options) {
|
|
|
3346
3620
|
// src/server/workspace/provisioning/skills.ts
|
|
3347
3621
|
import { stat as stat5 } from "fs/promises";
|
|
3348
3622
|
import { join as join7 } from "path";
|
|
3349
|
-
import { fileURLToPath as
|
|
3623
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
3350
3624
|
var GENERATED_SKILLS_REL = ".boring-agent/skills";
|
|
3351
3625
|
var USER_SKILLS_REL = ".agents/skills";
|
|
3352
3626
|
function sourceToPath2(source) {
|
|
3353
|
-
return source instanceof URL ?
|
|
3627
|
+
return source instanceof URL ? fileURLToPath4(source) : source;
|
|
3354
3628
|
}
|
|
3355
3629
|
function assertSafeSegment(kind, value) {
|
|
3356
3630
|
if (value.length === 0 || value.includes("\0") || value.includes("/") || value.includes("\\") || value === "." || value === "..") {
|
|
@@ -3392,9 +3666,9 @@ async function mirrorPluginSkills(options) {
|
|
|
3392
3666
|
import { basename, join as joinPath } from "path";
|
|
3393
3667
|
import { posix as posix2 } from "path";
|
|
3394
3668
|
import { readdir as readdir3, stat as stat6 } from "fs/promises";
|
|
3395
|
-
import { fileURLToPath as
|
|
3669
|
+
import { fileURLToPath as fileURLToPath5, pathToFileURL } from "url";
|
|
3396
3670
|
function sourceToPath3(source) {
|
|
3397
|
-
return source instanceof URL ?
|
|
3671
|
+
return source instanceof URL ? fileURLToPath5(source) : source;
|
|
3398
3672
|
}
|
|
3399
3673
|
function toWorkspaceRel3(...parts) {
|
|
3400
3674
|
return posix2.normalize(parts.filter(Boolean).join("/"));
|
|
@@ -3688,7 +3962,7 @@ async function provisionWorkspaceRuntime(opts) {
|
|
|
3688
3962
|
import { spawnSync } from "child_process";
|
|
3689
3963
|
|
|
3690
3964
|
// src/server/runtime/modes/direct.ts
|
|
3691
|
-
import { mkdir as
|
|
3965
|
+
import { mkdir as mkdir8 } from "fs/promises";
|
|
3692
3966
|
|
|
3693
3967
|
// src/server/runtime/createServerFileSearch.ts
|
|
3694
3968
|
var DEFAULT_LIMIT = 500;
|
|
@@ -3766,12 +4040,12 @@ async function copyTemplate(templatePath, workspaceRoot) {
|
|
|
3766
4040
|
|
|
3767
4041
|
// src/server/runtime/modes/provisioningAdapter.ts
|
|
3768
4042
|
import { spawn as spawn3 } from "child_process";
|
|
3769
|
-
import { cp as cp3, lstat as lstat3, mkdir as
|
|
3770
|
-
import { dirname as
|
|
3771
|
-
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";
|
|
3772
4046
|
var LOCAL_SANDBOX_WORKSPACE_ROOT = "/workspace";
|
|
3773
4047
|
function sourceToPath4(source) {
|
|
3774
|
-
return source instanceof URL ?
|
|
4048
|
+
return source instanceof URL ? fileURLToPath6(source) : source;
|
|
3775
4049
|
}
|
|
3776
4050
|
async function assertExistingInsideWorkspace(root, relPath, enforceSymlinkBoundary) {
|
|
3777
4051
|
const absPath = validatePath(root, relPath);
|
|
@@ -3789,9 +4063,9 @@ async function assertExistingInsideWorkspace(root, relPath, enforceSymlinkBounda
|
|
|
3789
4063
|
}
|
|
3790
4064
|
async function prepareWritablePath(root, relPath, enforceSymlinkBoundary) {
|
|
3791
4065
|
const absPath = validatePath(root, relPath);
|
|
3792
|
-
await
|
|
4066
|
+
await mkdir7(dirname10(absPath), { recursive: true });
|
|
3793
4067
|
if (enforceSymlinkBoundary) {
|
|
3794
|
-
await assertRealPathWithinWorkspace(root,
|
|
4068
|
+
await assertRealPathWithinWorkspace(root, dirname10(absPath));
|
|
3795
4069
|
}
|
|
3796
4070
|
try {
|
|
3797
4071
|
const targetStat = await lstat3(absPath);
|
|
@@ -3830,8 +4104,8 @@ async function spawnCommand(command, args, opts) {
|
|
|
3830
4104
|
stderr: Buffer.concat(stderr).toString("utf8")
|
|
3831
4105
|
});
|
|
3832
4106
|
};
|
|
3833
|
-
child.stdout?.on("data", (
|
|
3834
|
-
child.stderr?.on("data", (
|
|
4107
|
+
child.stdout?.on("data", (chunk3) => stdout.push(chunk3));
|
|
4108
|
+
child.stderr?.on("data", (chunk3) => stderr.push(chunk3));
|
|
3835
4109
|
child.on("error", settle);
|
|
3836
4110
|
child.on("close", (code) => {
|
|
3837
4111
|
if (code === 0) {
|
|
@@ -3855,7 +4129,7 @@ function defaultExecOptions(paths, opts) {
|
|
|
3855
4129
|
};
|
|
3856
4130
|
}
|
|
3857
4131
|
function mapWorkspacePathToLocalSandbox(paths, value) {
|
|
3858
|
-
const absolute =
|
|
4132
|
+
const absolute = isAbsolute6(value) ? value : resolve6(paths.workspaceRoot, value);
|
|
3859
4133
|
const relPath = relative8(paths.workspaceRoot, absolute);
|
|
3860
4134
|
if (relPath === "") return LOCAL_SANDBOX_WORKSPACE_ROOT;
|
|
3861
4135
|
if (relPath === ".." || relPath.startsWith(`..${sep5}`)) return value;
|
|
@@ -3869,33 +4143,18 @@ function mapEnvToLocalSandbox(paths, env) {
|
|
|
3869
4143
|
Object.entries(env).map(([key, value]) => [key, mapValueToLocalSandbox(paths, value)])
|
|
3870
4144
|
);
|
|
3871
4145
|
}
|
|
3872
|
-
function sanitizeInstallSourcePart(value) {
|
|
3873
|
-
const sanitized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3874
|
-
return sanitized.length > 0 ? sanitized : "source";
|
|
3875
|
-
}
|
|
3876
|
-
async function copyExternalSourceIntoWorkspace(paths, sourcePath, opts) {
|
|
3877
|
-
const fingerprint2 = opts.fingerprint.replace(/^sha256:/, "");
|
|
3878
|
-
const relTarget = `.boring-agent/tmp/${sanitizeInstallSourcePart(opts.kind)}-${sanitizeInstallSourcePart(opts.id)}-${sanitizeInstallSourcePart(fingerprint2)}-source`;
|
|
3879
|
-
const absTarget = validatePath(paths.workspaceRoot, relTarget);
|
|
3880
|
-
await rm3(absTarget, { recursive: true, force: true });
|
|
3881
|
-
await mkdir6(dirname9(absTarget), { recursive: true });
|
|
3882
|
-
await assertRealPathWithinWorkspace(paths.workspaceRoot, dirname9(absTarget));
|
|
3883
|
-
const sourceStat = await stat7(sourcePath);
|
|
3884
|
-
await cp3(sourcePath, absTarget, {
|
|
3885
|
-
recursive: sourceStat.isDirectory(),
|
|
3886
|
-
force: false,
|
|
3887
|
-
errorOnExist: true
|
|
3888
|
-
});
|
|
3889
|
-
return `${LOCAL_SANDBOX_WORKSPACE_ROOT}/${relTarget}`;
|
|
3890
|
-
}
|
|
3891
4146
|
function createWorkspaceFs(workspaceRoot, opts) {
|
|
3892
4147
|
const { enforceSymlinkBoundary } = opts;
|
|
3893
4148
|
return {
|
|
3894
4149
|
async exists(workspaceRelativePath) {
|
|
3895
|
-
const absPath =
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
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
|
+
}
|
|
3899
4158
|
},
|
|
3900
4159
|
async rm(workspaceRelativePath) {
|
|
3901
4160
|
const absPath = await assertExistingInsideWorkspace(workspaceRoot, workspaceRelativePath, enforceSymlinkBoundary);
|
|
@@ -3904,7 +4163,7 @@ function createWorkspaceFs(workspaceRoot, opts) {
|
|
|
3904
4163
|
},
|
|
3905
4164
|
async mkdir(workspaceRelativePath) {
|
|
3906
4165
|
const absPath = validatePath(workspaceRoot, workspaceRelativePath);
|
|
3907
|
-
await
|
|
4166
|
+
await mkdir7(absPath, { recursive: true });
|
|
3908
4167
|
if (enforceSymlinkBoundary) {
|
|
3909
4168
|
await assertRealPathWithinWorkspace(workspaceRoot, absPath);
|
|
3910
4169
|
}
|
|
@@ -3947,6 +4206,7 @@ function createDirectProvisioningAdapter(paths, runner = spawnCommand) {
|
|
|
3947
4206
|
}
|
|
3948
4207
|
function createLocalProvisioningAdapter(paths, runner = spawnCommand) {
|
|
3949
4208
|
const sourceMounts = /* @__PURE__ */ new Map();
|
|
4209
|
+
const workspaceFs = createWorkspaceFs(paths.workspaceRoot, { enforceSymlinkBoundary: true });
|
|
3950
4210
|
return {
|
|
3951
4211
|
mode: "local",
|
|
3952
4212
|
async exec(command, args, opts) {
|
|
@@ -3976,12 +4236,18 @@ function createLocalProvisioningAdapter(paths, runner = spawnCommand) {
|
|
|
3976
4236
|
const realSource = await realpath3(hostPath);
|
|
3977
4237
|
const relPath = relative8(realWorkspaceRoot, realSource);
|
|
3978
4238
|
if (relPath === "") return LOCAL_SANDBOX_WORKSPACE_ROOT;
|
|
3979
|
-
if (!relPath.startsWith("..") && !
|
|
4239
|
+
if (!relPath.startsWith("..") && !isAbsolute6(relPath)) {
|
|
3980
4240
|
return `${LOCAL_SANDBOX_WORKSPACE_ROOT}/${relPath.split(sep5).join("/")}`;
|
|
3981
4241
|
}
|
|
3982
|
-
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
|
+
});
|
|
3983
4249
|
},
|
|
3984
|
-
workspaceFs
|
|
4250
|
+
workspaceFs,
|
|
3985
4251
|
getRuntimeCacheRoot() {
|
|
3986
4252
|
return paths.cache;
|
|
3987
4253
|
}
|
|
@@ -3994,7 +4260,7 @@ var directModeAdapter = {
|
|
|
3994
4260
|
workspaceFsCapability: "strong",
|
|
3995
4261
|
createProvisioningAdapter: (runtimeLayout) => createDirectProvisioningAdapter(runtimeLayout),
|
|
3996
4262
|
async create(ctx) {
|
|
3997
|
-
await
|
|
4263
|
+
await mkdir8(ctx.workspaceRoot, { recursive: true });
|
|
3998
4264
|
await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
|
|
3999
4265
|
const runtimeContext = { runtimeCwd: ctx.workspaceRoot };
|
|
4000
4266
|
const workspace = createNodeWorkspace(ctx.workspaceRoot, { runtimeContext });
|
|
@@ -4011,7 +4277,7 @@ var directModeAdapter = {
|
|
|
4011
4277
|
};
|
|
4012
4278
|
|
|
4013
4279
|
// src/server/runtime/modes/local.ts
|
|
4014
|
-
import { mkdir as
|
|
4280
|
+
import { mkdir as mkdir9 } from "fs/promises";
|
|
4015
4281
|
var localModeAdapter = {
|
|
4016
4282
|
id: "local",
|
|
4017
4283
|
workspaceFsCapability: "strong",
|
|
@@ -4020,7 +4286,7 @@ var localModeAdapter = {
|
|
|
4020
4286
|
if (process.platform !== "linux") {
|
|
4021
4287
|
throw new Error("local mode requires Linux with bubblewrap");
|
|
4022
4288
|
}
|
|
4023
|
-
await
|
|
4289
|
+
await mkdir9(ctx.workspaceRoot, { recursive: true });
|
|
4024
4290
|
await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
|
|
4025
4291
|
const runtimeContext = { runtimeCwd: "/workspace" };
|
|
4026
4292
|
const workspace = createNodeWorkspace(ctx.workspaceRoot, { runtimeContext });
|
|
@@ -4040,11 +4306,9 @@ var localModeAdapter = {
|
|
|
4040
4306
|
};
|
|
4041
4307
|
|
|
4042
4308
|
// src/server/runtime/modes/vercel-sandbox.ts
|
|
4043
|
-
import {
|
|
4044
|
-
import {
|
|
4045
|
-
import {
|
|
4046
|
-
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
4047
|
-
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";
|
|
4048
4312
|
import { Sandbox as VercelSandbox } from "@vercel/sandbox";
|
|
4049
4313
|
|
|
4050
4314
|
// src/server/sandbox/vercel-sandbox/createVercelSandboxExec.ts
|
|
@@ -4055,15 +4319,15 @@ var DEFAULT_MAX_OUTPUT_BYTES3 = 1048576;
|
|
|
4055
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";
|
|
4056
4320
|
function createStreamWritable(collector, shared, onChunk) {
|
|
4057
4321
|
return new Writable({
|
|
4058
|
-
write(
|
|
4322
|
+
write(chunk3, _enc, cb) {
|
|
4059
4323
|
const remaining = shared.maxBytes - shared.totalBytes;
|
|
4060
4324
|
if (remaining <= 0) {
|
|
4061
4325
|
shared.truncated = true;
|
|
4062
4326
|
cb();
|
|
4063
4327
|
return;
|
|
4064
4328
|
}
|
|
4065
|
-
if (
|
|
4066
|
-
const partial =
|
|
4329
|
+
if (chunk3.length > remaining) {
|
|
4330
|
+
const partial = chunk3.subarray(0, remaining);
|
|
4067
4331
|
collector.chunks.push(partial);
|
|
4068
4332
|
shared.totalBytes += remaining;
|
|
4069
4333
|
shared.truncated = true;
|
|
@@ -4071,9 +4335,9 @@ function createStreamWritable(collector, shared, onChunk) {
|
|
|
4071
4335
|
cb();
|
|
4072
4336
|
return;
|
|
4073
4337
|
}
|
|
4074
|
-
collector.chunks.push(
|
|
4075
|
-
shared.totalBytes +=
|
|
4076
|
-
onChunk?.(new Uint8Array(
|
|
4338
|
+
collector.chunks.push(chunk3);
|
|
4339
|
+
shared.totalBytes += chunk3.length;
|
|
4340
|
+
onChunk?.(new Uint8Array(chunk3));
|
|
4077
4341
|
cb();
|
|
4078
4342
|
}
|
|
4079
4343
|
});
|
|
@@ -4257,11 +4521,11 @@ async function buildTarGz(files) {
|
|
|
4257
4521
|
}
|
|
4258
4522
|
chunks.push(Buffer.alloc(1024));
|
|
4259
4523
|
const tarBuffer = Buffer.concat(chunks);
|
|
4260
|
-
return new Promise((
|
|
4524
|
+
return new Promise((resolve11, reject) => {
|
|
4261
4525
|
const gzip = createGzip({ level: 6 });
|
|
4262
4526
|
const gzChunks = [];
|
|
4263
|
-
gzip.on("data", (
|
|
4264
|
-
gzip.on("end", () =>
|
|
4527
|
+
gzip.on("data", (chunk3) => gzChunks.push(chunk3));
|
|
4528
|
+
gzip.on("end", () => resolve11(Buffer.concat(gzChunks)));
|
|
4265
4529
|
gzip.on("error", reject);
|
|
4266
4530
|
Readable.from(tarBuffer).pipe(gzip);
|
|
4267
4531
|
});
|
|
@@ -4301,7 +4565,6 @@ var ORPHAN_GUARD_MAX_IDLE_MS = 24 * 60 * 60 * 1e3;
|
|
|
4301
4565
|
var VERCEL_SANDBOX_TIMEOUT_MS_ENV = "BORING_AGENT_VERCEL_SANDBOX_TIMEOUT_MS";
|
|
4302
4566
|
var VERCEL_SANDBOX_RUNTIME_ENV = "BORING_AGENT_VERCEL_SANDBOX_RUNTIME";
|
|
4303
4567
|
var DEFAULT_VERCEL_SANDBOX_RUNTIME = "node24";
|
|
4304
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
4305
4568
|
function sandboxTelemetryProperties(ctx, extra = {}) {
|
|
4306
4569
|
return {
|
|
4307
4570
|
runtimeMode: "vercel-sandbox",
|
|
@@ -4449,28 +4712,7 @@ async function seedTemplateIntoVercelWorkspace(workspace, templatePath, logger)
|
|
|
4449
4712
|
});
|
|
4450
4713
|
}
|
|
4451
4714
|
function provisioningSourceToPath(source) {
|
|
4452
|
-
return source instanceof URL ?
|
|
4453
|
-
}
|
|
4454
|
-
async function prepareVercelProvisioningArtifact(request) {
|
|
4455
|
-
const sourcePath = provisioningSourceToPath(request.source);
|
|
4456
|
-
await mkdir9(dirname10(request.outputPath), { recursive: true });
|
|
4457
|
-
if (request.kind === "node") {
|
|
4458
|
-
const { stdout } = await execFileAsync2("pnpm", [
|
|
4459
|
-
"--dir",
|
|
4460
|
-
sourcePath,
|
|
4461
|
-
"pack",
|
|
4462
|
-
"--pack-destination",
|
|
4463
|
-
dirname10(request.outputPath)
|
|
4464
|
-
], { maxBuffer: 1024 * 1024 * 20 });
|
|
4465
|
-
const packedName = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
4466
|
-
if (!packedName) throw new Error(`pnpm pack produced no artifact for ${sourcePath}`);
|
|
4467
|
-
const packedPath = isAbsolute6(packedName) ? packedName : join8(dirname10(request.outputPath), packedName);
|
|
4468
|
-
await rename5(packedPath, request.outputPath);
|
|
4469
|
-
return;
|
|
4470
|
-
}
|
|
4471
|
-
await execFileAsync2("tar", ["-czf", request.outputPath, "-C", sourcePath, "."], {
|
|
4472
|
-
maxBuffer: 1024 * 1024 * 20
|
|
4473
|
-
});
|
|
4715
|
+
return source instanceof URL ? fileURLToPath7(source) : source;
|
|
4474
4716
|
}
|
|
4475
4717
|
function shellSingleQuote(value) {
|
|
4476
4718
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -4720,7 +4962,7 @@ function createVercelSandboxModeAdapter(opts = {}) {
|
|
|
4720
4962
|
}
|
|
4721
4963
|
return { stdout, stderr };
|
|
4722
4964
|
},
|
|
4723
|
-
prepareArtifact:
|
|
4965
|
+
prepareArtifact: packProvisioningArtifact
|
|
4724
4966
|
});
|
|
4725
4967
|
},
|
|
4726
4968
|
async create(ctx) {
|
|
@@ -4901,8 +5143,8 @@ import Fastify from "fastify";
|
|
|
4901
5143
|
|
|
4902
5144
|
// src/server/harness/pi-coding-agent/createHarness.ts
|
|
4903
5145
|
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
4904
|
-
import { mkdir as
|
|
4905
|
-
import { extname as
|
|
5146
|
+
import { mkdir as mkdir12, writeFile as writeFile7 } from "fs/promises";
|
|
5147
|
+
import { extname as extname3, join as join10 } from "path";
|
|
4906
5148
|
import {
|
|
4907
5149
|
createAgentSession,
|
|
4908
5150
|
SessionManager,
|
|
@@ -5183,17 +5425,17 @@ import {
|
|
|
5183
5425
|
readFile as readFile9,
|
|
5184
5426
|
stat as fsStat,
|
|
5185
5427
|
rm as rm4,
|
|
5186
|
-
mkdir as
|
|
5428
|
+
mkdir as mkdir11,
|
|
5187
5429
|
writeFile as writeFile6,
|
|
5188
5430
|
appendFile,
|
|
5189
|
-
utimes
|
|
5431
|
+
utimes,
|
|
5432
|
+
open as open2
|
|
5190
5433
|
} from "fs/promises";
|
|
5191
5434
|
import { readFileSync, readdirSync } from "fs";
|
|
5192
|
-
import { join as join9 } from "path";
|
|
5435
|
+
import { join as join9, resolve as resolve7 } from "path";
|
|
5193
5436
|
import { homedir as homedir3 } from "os";
|
|
5194
5437
|
import {
|
|
5195
5438
|
parseSessionEntries,
|
|
5196
|
-
buildSessionContext,
|
|
5197
5439
|
CURRENT_SESSION_VERSION
|
|
5198
5440
|
} from "@mariozechner/pi-coding-agent";
|
|
5199
5441
|
function defaultSessionDir(cwd) {
|
|
@@ -5202,6 +5444,7 @@ function defaultSessionDir(cwd) {
|
|
|
5202
5444
|
}
|
|
5203
5445
|
var SAFE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
5204
5446
|
var SAFE_SESSION_NAMESPACE = /^[a-zA-Z0-9_-]+$/;
|
|
5447
|
+
var SUMMARY_PREFIX_BYTES = 64 * 1024;
|
|
5205
5448
|
function sessionDirForNamespace(namespace) {
|
|
5206
5449
|
const safeNamespace = namespace.trim();
|
|
5207
5450
|
if (!SAFE_SESSION_NAMESPACE.test(safeNamespace)) {
|
|
@@ -5223,16 +5466,28 @@ var PiSessionStore = class {
|
|
|
5223
5466
|
getSessionDir() {
|
|
5224
5467
|
return this.sessionDir;
|
|
5225
5468
|
}
|
|
5226
|
-
async list(ctx) {
|
|
5469
|
+
async list(ctx, options) {
|
|
5227
5470
|
const files = await readdir6(this.sessionDir).catch(() => []);
|
|
5228
5471
|
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
5472
|
+
const filepaths = jsonlFiles.map((f) => join9(this.sessionDir, f));
|
|
5473
|
+
const referencedPiFiles = await this.referencedPiFiles(filepaths);
|
|
5474
|
+
const fileStats = await Promise.all(filepaths.map(async (filepath) => {
|
|
5475
|
+
try {
|
|
5476
|
+
return { filepath, stat: await fsStat(filepath) };
|
|
5477
|
+
} catch {
|
|
5478
|
+
return null;
|
|
5479
|
+
}
|
|
5480
|
+
}));
|
|
5481
|
+
const visibleFiles = fileStats.filter((item) => item !== null).filter(({ filepath }) => !referencedPiFiles.has(resolve7(filepath))).sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
|
|
5482
|
+
const offset = Math.max(0, options?.offset ?? 0);
|
|
5483
|
+
const pageFiles = options?.limit === void 0 ? visibleFiles.slice(offset) : visibleFiles.slice(offset, offset + Math.max(0, options.limit));
|
|
5229
5484
|
const summaries = await Promise.all(
|
|
5230
|
-
|
|
5485
|
+
pageFiles.map(({ filepath, stat: stat11 }) => this.summarizeFile(filepath, stat11))
|
|
5231
5486
|
);
|
|
5232
|
-
return summaries.filter((s) => s !== null)
|
|
5487
|
+
return summaries.filter((s) => s !== null);
|
|
5233
5488
|
}
|
|
5234
5489
|
async create(ctx, init) {
|
|
5235
|
-
await
|
|
5490
|
+
await mkdir11(this.sessionDir, { recursive: true });
|
|
5236
5491
|
const id = randomUUID();
|
|
5237
5492
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5238
5493
|
const header = {
|
|
@@ -5279,17 +5534,25 @@ var PiSessionStore = class {
|
|
|
5279
5534
|
(e) => e.type !== "session"
|
|
5280
5535
|
);
|
|
5281
5536
|
const fileStat = await fsStat(filepath);
|
|
5537
|
+
const linkedPiFile = extractPiSessionFilePath(fileEntries);
|
|
5538
|
+
const linked = linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath) ? await this.readLinkedPiSession(linkedPiFile) : null;
|
|
5539
|
+
const linkedEntries = linked?.entries.filter(
|
|
5540
|
+
(e) => e.type !== "session"
|
|
5541
|
+
) ?? [];
|
|
5282
5542
|
const uiSnapshot = extractLatestUiSnapshot(fileEntries);
|
|
5283
|
-
const
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5543
|
+
const transcriptEntries = linkedEntries.length > 0 ? linkedEntries : sessionEntries;
|
|
5544
|
+
const messages = uiSnapshot ?? dropEmptyAssistantUiMessages(piMessagesToUIMessages(
|
|
5545
|
+
transcriptEntries.filter((entry) => entry.type === "message").map((entry) => entry.message),
|
|
5546
|
+
sessionId
|
|
5547
|
+
));
|
|
5548
|
+
const title = extractTitle(sessionEntries) ?? extractTitle(linkedEntries) ?? "New session";
|
|
5287
5549
|
const turnCount = messages.filter((m) => m.role === "user").length;
|
|
5550
|
+
const updatedAtMs = Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
|
|
5288
5551
|
return {
|
|
5289
5552
|
id: sessionId,
|
|
5290
5553
|
title,
|
|
5291
5554
|
createdAt: header?.timestamp ?? fileStat.birthtime.toISOString(),
|
|
5292
|
-
updatedAt:
|
|
5555
|
+
updatedAt: new Date(updatedAtMs).toISOString(),
|
|
5293
5556
|
turnCount,
|
|
5294
5557
|
messages
|
|
5295
5558
|
};
|
|
@@ -5300,7 +5563,7 @@ var PiSessionStore = class {
|
|
|
5300
5563
|
type: "ui_snapshot",
|
|
5301
5564
|
id: randomUUID(),
|
|
5302
5565
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5303
|
-
messages
|
|
5566
|
+
messages: sanitizeUiMessages(messages, { dropEmptyAssistantMessages: true })
|
|
5304
5567
|
});
|
|
5305
5568
|
await appendFile(filepath, entry + "\n");
|
|
5306
5569
|
}
|
|
@@ -5364,7 +5627,12 @@ var PiSessionStore = class {
|
|
|
5364
5627
|
const filepath = await this.resolveSessionFile(sessionId).catch(
|
|
5365
5628
|
() => null
|
|
5366
5629
|
);
|
|
5367
|
-
if (filepath)
|
|
5630
|
+
if (!filepath) return;
|
|
5631
|
+
const linkedPiFile = await this.linkedPiFileFor(filepath);
|
|
5632
|
+
await rm4(filepath, { force: true });
|
|
5633
|
+
if (linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath)) {
|
|
5634
|
+
await rm4(linkedPiFile, { force: true });
|
|
5635
|
+
}
|
|
5368
5636
|
}
|
|
5369
5637
|
touchSession(sessionId, title) {
|
|
5370
5638
|
this.resolveSessionFile(sessionId).then((filepath) => {
|
|
@@ -5400,11 +5668,33 @@ var PiSessionStore = class {
|
|
|
5400
5668
|
if (!match) throw new Error(`Session not found: ${sessionId}`);
|
|
5401
5669
|
return join9(this.sessionDir, match);
|
|
5402
5670
|
}
|
|
5403
|
-
async
|
|
5671
|
+
async linkedPiFileFor(filepath) {
|
|
5672
|
+
try {
|
|
5673
|
+
const content = await readFile9(filepath, "utf-8");
|
|
5674
|
+
return extractPiSessionFilePath(safeParseEntries(content));
|
|
5675
|
+
} catch {
|
|
5676
|
+
return null;
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
async referencedPiFiles(filepaths) {
|
|
5680
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
5681
|
+
await Promise.all(filepaths.map(async (filepath) => {
|
|
5682
|
+
try {
|
|
5683
|
+
const content = await readJsonlPrefix(filepath);
|
|
5684
|
+
const piFilePath = extractPiSessionFilePath(safeParseEntries(content));
|
|
5685
|
+
if (piFilePath && resolve7(piFilePath) !== resolve7(filepath)) {
|
|
5686
|
+
referenced.add(resolve7(piFilePath));
|
|
5687
|
+
}
|
|
5688
|
+
} catch {
|
|
5689
|
+
}
|
|
5690
|
+
}));
|
|
5691
|
+
return referenced;
|
|
5692
|
+
}
|
|
5693
|
+
async summarizeFile(filepath, existingStat) {
|
|
5404
5694
|
try {
|
|
5405
5695
|
const [fileStat, content] = await Promise.all([
|
|
5406
|
-
fsStat(filepath),
|
|
5407
|
-
|
|
5696
|
+
existingStat ?? fsStat(filepath),
|
|
5697
|
+
readJsonlPrefix(filepath)
|
|
5408
5698
|
]);
|
|
5409
5699
|
const firstNewline = content.indexOf("\n");
|
|
5410
5700
|
if (firstNewline === -1) return null;
|
|
@@ -5416,28 +5706,81 @@ var PiSessionStore = class {
|
|
|
5416
5706
|
const sessionEntries = entries.filter(
|
|
5417
5707
|
(e) => e.type !== "session"
|
|
5418
5708
|
);
|
|
5419
|
-
const
|
|
5420
|
-
const
|
|
5709
|
+
const linkedPiFile = extractPiSessionFilePath(entries);
|
|
5710
|
+
const linked = linkedPiFile && resolve7(linkedPiFile) !== resolve7(filepath) ? await this.readLinkedPiSessionSummary(linkedPiFile) : null;
|
|
5711
|
+
const linkedEntries = linked?.entries.filter(
|
|
5712
|
+
(e) => e.type !== "session"
|
|
5713
|
+
) ?? [];
|
|
5714
|
+
const title = extractTitle(sessionEntries) ?? extractTitle(linkedEntries) ?? firstUserMessage(linkedEntries) ?? firstUserMessage(sessionEntries) ?? "New session";
|
|
5715
|
+
const turnCount = [...sessionEntries, ...linkedEntries].filter(
|
|
5421
5716
|
(e) => e.type === "message" && e.message?.role === "user"
|
|
5422
5717
|
).length;
|
|
5718
|
+
const updatedAtMs = Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
|
|
5423
5719
|
return {
|
|
5424
5720
|
id: header.id,
|
|
5425
5721
|
title,
|
|
5426
5722
|
createdAt: header.timestamp,
|
|
5427
|
-
updatedAt:
|
|
5723
|
+
updatedAt: new Date(updatedAtMs).toISOString(),
|
|
5428
5724
|
turnCount
|
|
5429
5725
|
};
|
|
5430
5726
|
} catch {
|
|
5431
5727
|
return null;
|
|
5432
5728
|
}
|
|
5433
5729
|
}
|
|
5730
|
+
async readLinkedPiSession(filepath) {
|
|
5731
|
+
try {
|
|
5732
|
+
const [fileStat, content] = await Promise.all([
|
|
5733
|
+
fsStat(filepath),
|
|
5734
|
+
readFile9(filepath, "utf-8")
|
|
5735
|
+
]);
|
|
5736
|
+
return { entries: safeParseEntries(content), mtime: fileStat.mtime };
|
|
5737
|
+
} catch {
|
|
5738
|
+
return null;
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
async readLinkedPiSessionSummary(filepath) {
|
|
5742
|
+
try {
|
|
5743
|
+
const [fileStat, content] = await Promise.all([
|
|
5744
|
+
fsStat(filepath),
|
|
5745
|
+
readJsonlPrefix(filepath)
|
|
5746
|
+
]);
|
|
5747
|
+
return { entries: safeParseEntries(content), mtime: fileStat.mtime };
|
|
5748
|
+
} catch {
|
|
5749
|
+
return null;
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5434
5752
|
};
|
|
5753
|
+
async function readJsonlPrefix(filepath, maxBytes = SUMMARY_PREFIX_BYTES) {
|
|
5754
|
+
const handle = await open2(filepath, "r");
|
|
5755
|
+
try {
|
|
5756
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
5757
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
5758
|
+
let content = buffer.subarray(0, bytesRead).toString("utf-8");
|
|
5759
|
+
if (bytesRead === maxBytes) {
|
|
5760
|
+
const lastNewline = content.lastIndexOf("\n");
|
|
5761
|
+
if (lastNewline >= 0) content = content.slice(0, lastNewline + 1);
|
|
5762
|
+
}
|
|
5763
|
+
return content;
|
|
5764
|
+
} finally {
|
|
5765
|
+
await handle.close();
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
function extractPiSessionFilePath(entries) {
|
|
5769
|
+
let piFilePath = null;
|
|
5770
|
+
for (const e of entries) {
|
|
5771
|
+
const rec = e;
|
|
5772
|
+
if (rec.type === "pi_session_file" && typeof rec.path === "string") {
|
|
5773
|
+
piFilePath = rec.path;
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5776
|
+
return piFilePath;
|
|
5777
|
+
}
|
|
5435
5778
|
function extractLatestUiSnapshot(entries) {
|
|
5436
5779
|
let latest = null;
|
|
5437
5780
|
for (const e of entries) {
|
|
5438
5781
|
const rec = e;
|
|
5439
5782
|
if (rec.type === "ui_snapshot" && Array.isArray(rec.messages)) {
|
|
5440
|
-
latest = rec.messages;
|
|
5783
|
+
latest = sanitizeUiMessages(rec.messages, { dropEmptyAssistantMessages: true });
|
|
5441
5784
|
}
|
|
5442
5785
|
}
|
|
5443
5786
|
return latest;
|
|
@@ -5470,7 +5813,10 @@ function safeParseEntries(content) {
|
|
|
5470
5813
|
return results;
|
|
5471
5814
|
}
|
|
5472
5815
|
}
|
|
5473
|
-
function
|
|
5816
|
+
function reconstructedMessageId(sessionId, role, index) {
|
|
5817
|
+
return sessionId ? `${sessionId}-${role}-${index}` : `${role}-${index}`;
|
|
5818
|
+
}
|
|
5819
|
+
function piMessagesToUIMessages(messages, sessionId) {
|
|
5474
5820
|
const result = [];
|
|
5475
5821
|
let currentAssistant = null;
|
|
5476
5822
|
for (const raw of messages) {
|
|
@@ -5482,7 +5828,7 @@ function piMessagesToUIMessages(messages) {
|
|
|
5482
5828
|
const content = msg.content;
|
|
5483
5829
|
const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter((c) => c.type === "text").map((c) => c.text).join("") : "";
|
|
5484
5830
|
result.push({
|
|
5485
|
-
id:
|
|
5831
|
+
id: reconstructedMessageId(sessionId, "user", result.length),
|
|
5486
5832
|
role: "user",
|
|
5487
5833
|
parts: [{ type: "text", text }]
|
|
5488
5834
|
});
|
|
@@ -5516,7 +5862,7 @@ function piMessagesToUIMessages(messages) {
|
|
|
5516
5862
|
}
|
|
5517
5863
|
}
|
|
5518
5864
|
const uiMsg = {
|
|
5519
|
-
id:
|
|
5865
|
+
id: reconstructedMessageId(sessionId, "assistant", result.length),
|
|
5520
5866
|
role: "assistant",
|
|
5521
5867
|
parts
|
|
5522
5868
|
};
|
|
@@ -5554,7 +5900,7 @@ var DEFAULT_POLL_MS = 250;
|
|
|
5554
5900
|
var MAX_PROMPT_CHARS = 1200;
|
|
5555
5901
|
var MAX_TITLE_CHARS = 80;
|
|
5556
5902
|
function sleep(ms) {
|
|
5557
|
-
return new Promise((
|
|
5903
|
+
return new Promise((resolve11) => setTimeout(resolve11, ms));
|
|
5558
5904
|
}
|
|
5559
5905
|
function truncateForPrompt(text) {
|
|
5560
5906
|
const trimmed = text.trim();
|
|
@@ -6029,7 +6375,7 @@ async function applyRequestedSessionOptions(handle, input) {
|
|
|
6029
6375
|
var log3 = createLogger("pi-harness");
|
|
6030
6376
|
var DEFAULT_ATTACHMENT_DIR = "assets/images";
|
|
6031
6377
|
function extForAttachment(filename, contentType) {
|
|
6032
|
-
const fromName =
|
|
6378
|
+
const fromName = extname3(filename).toLowerCase().replace(/^\./, "");
|
|
6033
6379
|
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
6034
6380
|
if (contentType === "image/jpeg") return "jpg";
|
|
6035
6381
|
if (contentType === "image/png") return "png";
|
|
@@ -6189,6 +6535,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6189
6535
|
handle.piSession.dispose();
|
|
6190
6536
|
piSessions.delete(sessionId);
|
|
6191
6537
|
clearNativeFollowUpWork(sessionId);
|
|
6538
|
+
consumedNativeFollowUpKeys.delete(sessionId);
|
|
6192
6539
|
}
|
|
6193
6540
|
const originalDelete = sessionStore.delete.bind(sessionStore);
|
|
6194
6541
|
sessionStore.delete = async (ctx, sessionId) => {
|
|
@@ -6197,6 +6544,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6197
6544
|
};
|
|
6198
6545
|
const nativeFollowUpPending = /* @__PURE__ */ new Set();
|
|
6199
6546
|
const nativeFollowUpQueues = /* @__PURE__ */ new Map();
|
|
6547
|
+
const consumedNativeFollowUpKeys = /* @__PURE__ */ new Map();
|
|
6200
6548
|
function clearNativeFollowUpWork(sessionId) {
|
|
6201
6549
|
nativeFollowUpPending.delete(sessionId);
|
|
6202
6550
|
nativeFollowUpQueues.delete(sessionId);
|
|
@@ -6232,12 +6580,38 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6232
6580
|
}
|
|
6233
6581
|
session._emitQueueUpdate?.();
|
|
6234
6582
|
}
|
|
6583
|
+
function followUpDedupeKeys(options) {
|
|
6584
|
+
const keys = [];
|
|
6585
|
+
if (options?.clientNonce) keys.push(`nonce:${options.clientNonce}`);
|
|
6586
|
+
if (options?.clientSeq !== void 0) keys.push(`seq:${options.clientSeq}`);
|
|
6587
|
+
return keys;
|
|
6588
|
+
}
|
|
6235
6589
|
function hasFollowUpSelector(options) {
|
|
6236
|
-
return
|
|
6590
|
+
return followUpDedupeKeys(options).length > 0;
|
|
6237
6591
|
}
|
|
6238
6592
|
function matchesFollowUpSelector(item, options) {
|
|
6239
6593
|
if (!hasFollowUpSelector(options)) return true;
|
|
6240
|
-
|
|
6594
|
+
const optionKeys = new Set(followUpDedupeKeys(options));
|
|
6595
|
+
return followUpDedupeKeys(item).some((key) => optionKeys.has(key));
|
|
6596
|
+
}
|
|
6597
|
+
function rememberConsumedNativeFollowUp(sessionId, request) {
|
|
6598
|
+
const keys = followUpDedupeKeys(request);
|
|
6599
|
+
if (keys.length === 0) return;
|
|
6600
|
+
const consumed = consumedNativeFollowUpKeys.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
6601
|
+
for (const key of keys) consumed.add(key);
|
|
6602
|
+
consumedNativeFollowUpKeys.set(sessionId, consumed);
|
|
6603
|
+
}
|
|
6604
|
+
function followUpPostDedupeKeys(options) {
|
|
6605
|
+
if (options?.clientNonce) return [`nonce:${options.clientNonce}`];
|
|
6606
|
+
if (options?.clientSeq !== void 0) return [`seq:${options.clientSeq}`];
|
|
6607
|
+
return [];
|
|
6608
|
+
}
|
|
6609
|
+
function hasQueuedOrConsumedNativeFollowUp(sessionId, options) {
|
|
6610
|
+
const optionKeys = new Set(followUpPostDedupeKeys(options));
|
|
6611
|
+
if (optionKeys.size === 0) return false;
|
|
6612
|
+
const consumed = consumedNativeFollowUpKeys.get(sessionId);
|
|
6613
|
+
if (consumed && [...optionKeys].some((key) => consumed.has(key))) return true;
|
|
6614
|
+
return (nativeFollowUpQueues.get(sessionId) ?? []).some((request) => followUpPostDedupeKeys(request).some((key) => optionKeys.has(key)));
|
|
6241
6615
|
}
|
|
6242
6616
|
function removeNativeFollowUp(sessionId, options) {
|
|
6243
6617
|
const queue = nativeFollowUpQueues.get(sessionId);
|
|
@@ -6265,6 +6639,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6265
6639
|
async followUp(sessionId, text, _attachments, displayText = text, options) {
|
|
6266
6640
|
const handle = piSessions.get(sessionId);
|
|
6267
6641
|
if (!handle) throw new Error("followup_session_not_ready");
|
|
6642
|
+
if (hasQueuedOrConsumedNativeFollowUp(sessionId, options)) return;
|
|
6268
6643
|
const queue = nativeFollowUpQueues.get(sessionId) ?? [];
|
|
6269
6644
|
queue.push({
|
|
6270
6645
|
text,
|
|
@@ -6369,34 +6744,34 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6369
6744
|
"tool-output-error"
|
|
6370
6745
|
]);
|
|
6371
6746
|
const standardToolInputsSeen = /* @__PURE__ */ new Set();
|
|
6372
|
-
function isStandardVisibleChunk(
|
|
6373
|
-
const type =
|
|
6747
|
+
function isStandardVisibleChunk(chunk3) {
|
|
6748
|
+
const type = chunk3.type;
|
|
6374
6749
|
return typeof type === "string" && STANDARD_VISIBLE_CHUNK_TYPES.has(type);
|
|
6375
6750
|
}
|
|
6376
6751
|
function namespaceInlinePartIds(input2) {
|
|
6377
6752
|
if (inlineTurnIndex === 0) return input2;
|
|
6378
|
-
return input2.map((
|
|
6379
|
-
const rec =
|
|
6753
|
+
return input2.map((chunk3) => {
|
|
6754
|
+
const rec = chunk3;
|
|
6380
6755
|
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") {
|
|
6381
6756
|
return { ...rec, id: `turn-${inlineTurnIndex}:${rec.id}` };
|
|
6382
6757
|
}
|
|
6383
|
-
return
|
|
6758
|
+
return chunk3;
|
|
6384
6759
|
});
|
|
6385
6760
|
}
|
|
6386
6761
|
function filterSdkChunksForCurrentSegment(input2) {
|
|
6387
6762
|
const out = [];
|
|
6388
|
-
for (const
|
|
6389
|
-
const rec =
|
|
6390
|
-
if (inlineTurnIndex > 0 && isStandardVisibleChunk(
|
|
6763
|
+
for (const chunk3 of input2) {
|
|
6764
|
+
const rec = chunk3;
|
|
6765
|
+
if (inlineTurnIndex > 0 && isStandardVisibleChunk(chunk3)) continue;
|
|
6391
6766
|
if (rec.type === "tool-input-available" && rec.toolCallId) {
|
|
6392
6767
|
standardToolInputsSeen.add(rec.toolCallId);
|
|
6393
|
-
out.push(
|
|
6768
|
+
out.push(chunk3);
|
|
6394
6769
|
continue;
|
|
6395
6770
|
}
|
|
6396
6771
|
if ((rec.type === "tool-output-available" || rec.type === "tool-output-error" || rec.type === "tool-output-denied") && rec.toolCallId && !standardToolInputsSeen.has(rec.toolCallId)) {
|
|
6397
6772
|
continue;
|
|
6398
6773
|
}
|
|
6399
|
-
out.push(
|
|
6774
|
+
out.push(chunk3);
|
|
6400
6775
|
}
|
|
6401
6776
|
return out;
|
|
6402
6777
|
}
|
|
@@ -6528,8 +6903,8 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6528
6903
|
const queue = nativeFollowUpQueues.get(input.sessionId);
|
|
6529
6904
|
if (queue?.length) {
|
|
6530
6905
|
const index = queue.findIndex((item) => item.text === text || item.displayText === text);
|
|
6531
|
-
|
|
6532
|
-
|
|
6906
|
+
const consumed = index >= 0 ? queue.splice(index, 1)[0] : queue.shift();
|
|
6907
|
+
rememberConsumedNativeFollowUp(input.sessionId, consumed);
|
|
6533
6908
|
if (queue.length > 0) nativeFollowUpQueues.set(input.sessionId, queue);
|
|
6534
6909
|
else clearNativeFollowUpWork(input.sessionId);
|
|
6535
6910
|
} else {
|
|
@@ -6541,10 +6916,10 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6541
6916
|
} else {
|
|
6542
6917
|
const sdkChunks = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
|
|
6543
6918
|
const shouldBufferTerminalError = event.type === "message_update" && event.assistantMessageEvent?.type === "error";
|
|
6544
|
-
const visibleSdkChunks = shouldBufferTerminalError ? sdkChunks.filter((
|
|
6545
|
-
const type =
|
|
6919
|
+
const visibleSdkChunks = shouldBufferTerminalError ? sdkChunks.filter((chunk3) => {
|
|
6920
|
+
const type = chunk3.type;
|
|
6546
6921
|
if (type === "error" || type === "finish") {
|
|
6547
|
-
pendingTerminalErrorChunks.push(
|
|
6922
|
+
pendingTerminalErrorChunks.push(chunk3);
|
|
6548
6923
|
return false;
|
|
6549
6924
|
}
|
|
6550
6925
|
return true;
|
|
@@ -6552,12 +6927,12 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6552
6927
|
const sdkChunksForTurn = filterSdkChunksForCurrentSegment(visibleSdkChunks);
|
|
6553
6928
|
converted = [...piHistoryChunks, ...sdkChunksForTurn];
|
|
6554
6929
|
}
|
|
6555
|
-
for (const
|
|
6556
|
-
const t =
|
|
6930
|
+
for (const chunk3 of converted) {
|
|
6931
|
+
const t = chunk3.type;
|
|
6557
6932
|
if (t === "text-delta" || t === "data-pi-text-delta" || t === "data-pi-text-end") {
|
|
6558
6933
|
sawTextChunk = true;
|
|
6559
6934
|
}
|
|
6560
|
-
const piEndData =
|
|
6935
|
+
const piEndData = chunk3.data;
|
|
6561
6936
|
if (t === "data-pi-message-end" && piEndData?.role === "assistant" && typeof piEndData.text === "string" && piEndData.text.length > 0) {
|
|
6562
6937
|
sawTextChunk = true;
|
|
6563
6938
|
}
|
|
@@ -6626,7 +7001,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
6626
7001
|
const base = basenameForAttachment(a.filename ?? "image");
|
|
6627
7002
|
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
6628
7003
|
const relPath = `${DEFAULT_ATTACHMENT_DIR}/${base}-${unique}.${ext}`;
|
|
6629
|
-
await
|
|
7004
|
+
await mkdir12(join10(opts.cwd, DEFAULT_ATTACHMENT_DIR), { recursive: true });
|
|
6630
7005
|
await writeFile7(join10(opts.cwd, relPath), bytes);
|
|
6631
7006
|
savedPaths.push(relPath);
|
|
6632
7007
|
} catch (err) {
|
|
@@ -6717,7 +7092,7 @@ ${attachmentNotes.join("\n")}` : message,
|
|
|
6717
7092
|
|
|
6718
7093
|
// src/server/harness/pi-coding-agent/pluginLoader.ts
|
|
6719
7094
|
import { readdir as readdir7, stat as stat9, readFile as readFile10 } from "fs/promises";
|
|
6720
|
-
import { join as join11, extname as
|
|
7095
|
+
import { join as join11, extname as extname4, resolve as resolve8, sep as sep6, relative as relative9 } from "path";
|
|
6721
7096
|
import { homedir as homedir4 } from "os";
|
|
6722
7097
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
6723
7098
|
var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
|
|
@@ -6743,7 +7118,7 @@ async function fileExists(path4) {
|
|
|
6743
7118
|
async function discoverFromDir(dir, source) {
|
|
6744
7119
|
if (!await dirExists2(dir)) return [];
|
|
6745
7120
|
const entries = await readdir7(dir);
|
|
6746
|
-
return entries.filter((e) => VALID_EXTENSIONS.has(
|
|
7121
|
+
return entries.filter((e) => VALID_EXTENSIONS.has(extname4(e))).map((e) => ({ path: join11(dir, e), source }));
|
|
6747
7122
|
}
|
|
6748
7123
|
function extractTools(mod) {
|
|
6749
7124
|
const tools = [];
|
|
@@ -6787,8 +7162,8 @@ async function loadNpmPlugin(pkgDir, importFn) {
|
|
|
6787
7162
|
if (!await fileExists(pkgJsonPath)) return [];
|
|
6788
7163
|
const pkgJson = JSON.parse(await readFile10(pkgJsonPath, "utf-8"));
|
|
6789
7164
|
const main = pkgJson.main ?? "index.js";
|
|
6790
|
-
const resolvedMain =
|
|
6791
|
-
const resolvedPkgDir =
|
|
7165
|
+
const resolvedMain = resolve8(pkgDir, main);
|
|
7166
|
+
const resolvedPkgDir = resolve8(pkgDir);
|
|
6792
7167
|
if (resolvedMain !== resolvedPkgDir && !resolvedMain.startsWith(resolvedPkgDir + sep6)) {
|
|
6793
7168
|
return [];
|
|
6794
7169
|
}
|
|
@@ -6886,8 +7261,8 @@ function getRuntimeBundleStorageRoot(bundle) {
|
|
|
6886
7261
|
|
|
6887
7262
|
// src/server/tools/operations/bound.ts
|
|
6888
7263
|
import { constants as constants4 } from "fs";
|
|
6889
|
-
import { access as access4, lstat as lstat4, mkdir as
|
|
6890
|
-
import { dirname as dirname11, isAbsolute as isAbsolute7, join as join12, relative as relative10, resolve as
|
|
7264
|
+
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";
|
|
7265
|
+
import { dirname as dirname11, isAbsolute as isAbsolute7, join as join12, relative as relative10, resolve as resolve9 } from "path";
|
|
6891
7266
|
function toPosixPath(value) {
|
|
6892
7267
|
return value.split("\\").join("/");
|
|
6893
7268
|
}
|
|
@@ -6943,7 +7318,7 @@ async function walkMatches(root, current, pattern, ignore, limit, out) {
|
|
|
6943
7318
|
const entries = await readdir8(current, { withFileTypes: true });
|
|
6944
7319
|
for (const entry of entries) {
|
|
6945
7320
|
if (out.length >= limit) return;
|
|
6946
|
-
const absolutePath =
|
|
7321
|
+
const absolutePath = resolve9(current, entry.name);
|
|
6947
7322
|
const relativePath = toPosixPath(relative10(root, absolutePath));
|
|
6948
7323
|
if (entry.isDirectory() && shouldSkipDir(relativePath, ignore)) continue;
|
|
6949
7324
|
if (matchesGlob(relativePath, pattern)) {
|
|
@@ -6968,12 +7343,12 @@ async function findNearestExistingAncestor(absPath) {
|
|
|
6968
7343
|
}
|
|
6969
7344
|
}
|
|
6970
7345
|
async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
6971
|
-
const realRoot = await realpath4(
|
|
7346
|
+
const realRoot = await realpath4(resolve9(workspaceRoot));
|
|
6972
7347
|
try {
|
|
6973
7348
|
const s = await lstat4(absPath);
|
|
6974
7349
|
if (s.isSymbolicLink()) {
|
|
6975
7350
|
const target = await readlink(absPath);
|
|
6976
|
-
const resolvedTarget =
|
|
7351
|
+
const resolvedTarget = resolve9(dirname11(absPath), target);
|
|
6977
7352
|
const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
|
|
6978
7353
|
const realAncestor = await realpath4(nearestAncestor);
|
|
6979
7354
|
const rel2 = relative10(realRoot, realAncestor);
|
|
@@ -7045,13 +7420,13 @@ function boundFs(workspaceRoot, opts = {}) {
|
|
|
7045
7420
|
async writeFile(absolutePath, content) {
|
|
7046
7421
|
const storagePath = toStoragePath(absolutePath);
|
|
7047
7422
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7048
|
-
await
|
|
7423
|
+
await mkdir13(dirname11(storagePath), { recursive: true });
|
|
7049
7424
|
await writeFile8(storagePath, content);
|
|
7050
7425
|
},
|
|
7051
7426
|
async mkdir(dir) {
|
|
7052
7427
|
const storagePath = toStoragePath(dir);
|
|
7053
7428
|
await assertWithinWorkspace(workspaceRoot, storagePath);
|
|
7054
|
-
await
|
|
7429
|
+
await mkdir13(storagePath, { recursive: true });
|
|
7055
7430
|
}
|
|
7056
7431
|
};
|
|
7057
7432
|
const edit = {
|
|
@@ -7168,8 +7543,8 @@ function vercelBashOps(sandbox, opts = {}) {
|
|
|
7168
7543
|
env: filteredEnv,
|
|
7169
7544
|
signal,
|
|
7170
7545
|
timeoutMs: timeout ? timeout * 1e3 : void 0,
|
|
7171
|
-
onStdout: (
|
|
7172
|
-
onStderr: (
|
|
7546
|
+
onStdout: (chunk3) => onData(Buffer.from(chunk3)),
|
|
7547
|
+
onStderr: (chunk3) => onData(Buffer.from(chunk3))
|
|
7173
7548
|
}).then((result) => ({ exitCode: result.exitCode }));
|
|
7174
7549
|
}
|
|
7175
7550
|
};
|
|
@@ -7336,7 +7711,7 @@ import {
|
|
|
7336
7711
|
truncateHead,
|
|
7337
7712
|
truncateLine
|
|
7338
7713
|
} from "@mariozechner/pi-coding-agent";
|
|
7339
|
-
import { resolve as
|
|
7714
|
+
import { resolve as resolve10, relative as relative12 } from "path";
|
|
7340
7715
|
|
|
7341
7716
|
// src/server/catalog/tools/_shared.ts
|
|
7342
7717
|
function makeError(message) {
|
|
@@ -7488,7 +7863,7 @@ function vercelGrepTool(sandbox, workspaceRoot) {
|
|
|
7488
7863
|
return makeError("pattern is required");
|
|
7489
7864
|
}
|
|
7490
7865
|
if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
|
|
7491
|
-
const resolved =
|
|
7866
|
+
const resolved = resolve10(workspaceRoot, params.path);
|
|
7492
7867
|
const rel = relative12(workspaceRoot, resolved);
|
|
7493
7868
|
if (rel.startsWith("..") || rel.startsWith("/")) {
|
|
7494
7869
|
return makeError(`path "${params.path}" is outside workspace`);
|
|
@@ -7576,6 +7951,84 @@ import {
|
|
|
7576
7951
|
createBashToolDefinition,
|
|
7577
7952
|
createLocalBashOperations
|
|
7578
7953
|
} from "@mariozechner/pi-coding-agent";
|
|
7954
|
+
|
|
7955
|
+
// src/server/catalog/toolReadiness.ts
|
|
7956
|
+
var WORKSPACE_PREPARING_MESSAGE = "Workspace is still preparing. Try again in a moment.";
|
|
7957
|
+
function isRuntimeRequirement(requirement) {
|
|
7958
|
+
return requirement === "runtime-dependencies" || requirement.startsWith("runtime:");
|
|
7959
|
+
}
|
|
7960
|
+
function isReadyState(state) {
|
|
7961
|
+
return state === true || typeof state === "object" && state !== null && state.ready === true;
|
|
7962
|
+
}
|
|
7963
|
+
function blockedState(state) {
|
|
7964
|
+
if (state && typeof state === "object" && state.ready === false) return state;
|
|
7965
|
+
return { ready: false, state: "preparing", retryable: true };
|
|
7966
|
+
}
|
|
7967
|
+
function runtimeRequirementMessage(requirement, state) {
|
|
7968
|
+
if (state === "failed") {
|
|
7969
|
+
return "Runtime setup failed. Retry provisioning or reload the workspace.";
|
|
7970
|
+
}
|
|
7971
|
+
switch (requirement) {
|
|
7972
|
+
case "runtime:python":
|
|
7973
|
+
return "Python runtime dependencies are still installing. This usually takes a few seconds.";
|
|
7974
|
+
case "runtime:node":
|
|
7975
|
+
return "Node runtime dependencies are still installing. This usually takes a few seconds.";
|
|
7976
|
+
default:
|
|
7977
|
+
return "Runtime dependencies are still installing. This usually takes a few seconds.";
|
|
7978
|
+
}
|
|
7979
|
+
}
|
|
7980
|
+
function workspaceNotReadyToolResult(requirement) {
|
|
7981
|
+
return {
|
|
7982
|
+
content: [{ type: "text", text: WORKSPACE_PREPARING_MESSAGE }],
|
|
7983
|
+
isError: true,
|
|
7984
|
+
details: {
|
|
7985
|
+
code: ErrorCode.enum.WORKSPACE_NOT_READY,
|
|
7986
|
+
retryable: true,
|
|
7987
|
+
requirement
|
|
7988
|
+
}
|
|
7989
|
+
};
|
|
7990
|
+
}
|
|
7991
|
+
function runtimeNotReadyToolResult(requirement, state = { ready: false, state: "preparing", retryable: true }) {
|
|
7992
|
+
const readinessState = state.state ?? "preparing";
|
|
7993
|
+
const code = readinessState === "failed" ? ErrorCode.enum.RUNTIME_PROVISIONING_FAILED : ErrorCode.enum.AGENT_RUNTIME_NOT_READY;
|
|
7994
|
+
const message = state.message ?? runtimeRequirementMessage(requirement, readinessState);
|
|
7995
|
+
return {
|
|
7996
|
+
content: [{ type: "text", text: message }],
|
|
7997
|
+
isError: true,
|
|
7998
|
+
details: {
|
|
7999
|
+
code,
|
|
8000
|
+
retryable: state.retryable ?? true,
|
|
8001
|
+
requirement,
|
|
8002
|
+
state: readinessState,
|
|
8003
|
+
...state.workspaceId ? { workspaceId: state.workspaceId } : {},
|
|
8004
|
+
...state.errorCode ? { errorCode: state.errorCode } : {},
|
|
8005
|
+
...state.causeCode ? { causeCode: state.causeCode } : {}
|
|
8006
|
+
}
|
|
8007
|
+
};
|
|
8008
|
+
}
|
|
8009
|
+
function readinessToolResult(requirement, state) {
|
|
8010
|
+
if (isRuntimeRequirement(requirement)) return runtimeNotReadyToolResult(requirement, blockedState(state));
|
|
8011
|
+
return workspaceNotReadyToolResult(requirement);
|
|
8012
|
+
}
|
|
8013
|
+
function withReadinessRequirements(tool, readinessRequirements) {
|
|
8014
|
+
if (tool.readinessRequirements === readinessRequirements) return tool;
|
|
8015
|
+
return { ...tool, readinessRequirements };
|
|
8016
|
+
}
|
|
8017
|
+
function wrapToolForReadiness(tool, checkReadiness) {
|
|
8018
|
+
if (!checkReadiness || !tool.readinessRequirements || tool.readinessRequirements.length === 0) return tool;
|
|
8019
|
+
return {
|
|
8020
|
+
...tool,
|
|
8021
|
+
async execute(params, ctx) {
|
|
8022
|
+
for (const requirement of tool.readinessRequirements ?? []) {
|
|
8023
|
+
const readiness = checkReadiness(requirement, tool);
|
|
8024
|
+
if (!isReadyState(readiness)) return readinessToolResult(requirement, readiness);
|
|
8025
|
+
}
|
|
8026
|
+
return await tool.execute(params, ctx);
|
|
8027
|
+
}
|
|
8028
|
+
};
|
|
8029
|
+
}
|
|
8030
|
+
|
|
8031
|
+
// src/server/tools/harness/index.ts
|
|
7579
8032
|
function shellEscape2(s) {
|
|
7580
8033
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
7581
8034
|
}
|
|
@@ -7639,27 +8092,77 @@ function bashOptionsForMode(bundle, runtime) {
|
|
|
7639
8092
|
};
|
|
7640
8093
|
}
|
|
7641
8094
|
}
|
|
7642
|
-
function
|
|
8095
|
+
function isRuntimeReady(readiness) {
|
|
8096
|
+
return readiness === void 0 || readiness === true || typeof readiness === "object" && readiness !== null && readiness.ready === true;
|
|
8097
|
+
}
|
|
8098
|
+
function runtimeRequirementForCommand(command) {
|
|
8099
|
+
if (command.includes(".boring-agent/venv/bin/")) return "runtime:python";
|
|
8100
|
+
if (/python(?:3)?\s+-c\s+['\"][^'\"]*(?:from\s+\S+\s+)?import\s+/.test(command)) return "runtime:python";
|
|
8101
|
+
if (command.includes(".boring-agent/node/")) return "runtime:node";
|
|
8102
|
+
if (command.includes(".boring-agent/")) return "runtime-dependencies";
|
|
8103
|
+
return void 0;
|
|
8104
|
+
}
|
|
8105
|
+
function runtimeRequirementForFailure(text) {
|
|
8106
|
+
if (/\.boring-agent\/venv\/bin\/[^\s:]+: (?:No such file or directory|not found)/i.test(text)) return "runtime:python";
|
|
8107
|
+
if (/ModuleNotFoundError: No module named ['\"][^'\"]+['\"]/i.test(text)) return "runtime:python";
|
|
8108
|
+
if (/\.boring-agent\/node\/[^\s:]+: (?:No such file or directory|not found)/i.test(text)) return "runtime:node";
|
|
8109
|
+
if (/(?:^|\n)(?:[^\n:]+:\s*)?(?:line \d+:\s*)?[A-Za-z0-9_.-]+: command not found/i.test(text)) return "runtime-dependencies";
|
|
8110
|
+
return void 0;
|
|
8111
|
+
}
|
|
8112
|
+
function adaptPiTool2(piTool, runtime) {
|
|
7643
8113
|
return {
|
|
7644
8114
|
name: piTool.name,
|
|
7645
8115
|
description: piTool.description,
|
|
7646
8116
|
promptSnippet: piTool.promptSnippet,
|
|
7647
8117
|
parameters: piTool.parameters,
|
|
8118
|
+
readinessRequirements: ["sandbox-exec"],
|
|
7648
8119
|
async execute(params, ctx) {
|
|
7649
|
-
const
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
8120
|
+
const command = typeof params.command === "string" ? params.command : "";
|
|
8121
|
+
const readiness = runtime?.getReadiness?.();
|
|
8122
|
+
const commandRuntimeRequirement = command ? runtimeRequirementForCommand(command) : void 0;
|
|
8123
|
+
if (commandRuntimeRequirement && !isRuntimeReady(readiness)) {
|
|
8124
|
+
return runtimeNotReadyToolResult(
|
|
8125
|
+
commandRuntimeRequirement,
|
|
8126
|
+
readiness && typeof readiness === "object" && readiness.ready === false ? readiness : { ready: false, state: "preparing", retryable: true }
|
|
8127
|
+
);
|
|
8128
|
+
}
|
|
8129
|
+
let result;
|
|
8130
|
+
try {
|
|
8131
|
+
result = await piTool.execute(
|
|
8132
|
+
ctx.toolCallId,
|
|
8133
|
+
params,
|
|
8134
|
+
ctx.abortSignal,
|
|
8135
|
+
ctx.onUpdate ? (update) => {
|
|
8136
|
+
const text2 = update.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
8137
|
+
ctx.onUpdate(text2);
|
|
8138
|
+
} : void 0,
|
|
8139
|
+
{}
|
|
8140
|
+
);
|
|
8141
|
+
} catch (error) {
|
|
8142
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8143
|
+
const latestReadiness2 = runtime?.getReadiness?.();
|
|
8144
|
+
const failureRuntimeRequirement2 = runtimeRequirementForFailure(message);
|
|
8145
|
+
if (command && failureRuntimeRequirement2 && !isRuntimeReady(latestReadiness2)) {
|
|
8146
|
+
return runtimeNotReadyToolResult(
|
|
8147
|
+
failureRuntimeRequirement2,
|
|
8148
|
+
latestReadiness2 && typeof latestReadiness2 === "object" && latestReadiness2.ready === false ? latestReadiness2 : { ready: false, state: "preparing", retryable: true }
|
|
8149
|
+
);
|
|
8150
|
+
}
|
|
8151
|
+
throw error;
|
|
8152
|
+
}
|
|
7659
8153
|
const textContent = (result.content ?? []).filter((c) => c.type === "text").map((c) => ({ type: "text", text: c.text }));
|
|
8154
|
+
const text = textContent.map((part) => part.text).join("\n");
|
|
8155
|
+
const latestReadiness = runtime?.getReadiness?.();
|
|
8156
|
+
const failureRuntimeRequirement = runtimeRequirementForFailure(text);
|
|
8157
|
+
if (command && failureRuntimeRequirement && !isRuntimeReady(latestReadiness)) {
|
|
8158
|
+
return runtimeNotReadyToolResult(
|
|
8159
|
+
failureRuntimeRequirement,
|
|
8160
|
+
latestReadiness && typeof latestReadiness === "object" && latestReadiness.ready === false ? latestReadiness : { ready: false, state: "preparing", retryable: true }
|
|
8161
|
+
);
|
|
8162
|
+
}
|
|
7660
8163
|
return {
|
|
7661
8164
|
content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
|
|
7662
|
-
isError:
|
|
8165
|
+
isError: Boolean(result.isError),
|
|
7663
8166
|
details: result.details
|
|
7664
8167
|
};
|
|
7665
8168
|
}
|
|
@@ -7720,7 +8223,7 @@ function createExecuteIsolatedCodeTool(sandbox) {
|
|
|
7720
8223
|
}
|
|
7721
8224
|
function buildHarnessAgentTools(bundle, runtime) {
|
|
7722
8225
|
const tools = [
|
|
7723
|
-
adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle, runtime)))
|
|
8226
|
+
adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle, runtime)), runtime)
|
|
7724
8227
|
];
|
|
7725
8228
|
if (bundle.sandbox.capabilities.includes("isolated-code")) {
|
|
7726
8229
|
tools.push(createExecuteIsolatedCodeTool(bundle.sandbox));
|
|
@@ -8048,9 +8551,9 @@ var TurnBuffer = class {
|
|
|
8048
8551
|
onChunk = /* @__PURE__ */ new Set();
|
|
8049
8552
|
onDone = /* @__PURE__ */ new Set();
|
|
8050
8553
|
gc = null;
|
|
8051
|
-
append(
|
|
8554
|
+
append(chunk3) {
|
|
8052
8555
|
const idx = this.nextIdx++;
|
|
8053
|
-
const entry = { idx, chunk:
|
|
8556
|
+
const entry = { idx, chunk: chunk3 };
|
|
8054
8557
|
this.ring.push(entry);
|
|
8055
8558
|
if (this.ring.length > MAX_CHUNKS) this.ring.shift();
|
|
8056
8559
|
for (const h of this.onChunk) h(entry);
|
|
@@ -8165,11 +8668,11 @@ var VALID_OPS = /* @__PURE__ */ new Set([
|
|
|
8165
8668
|
function isRecord2(value) {
|
|
8166
8669
|
return typeof value === "object" && value !== null;
|
|
8167
8670
|
}
|
|
8168
|
-
function parseFileChangeChunk(
|
|
8169
|
-
if (!isRecord2(
|
|
8671
|
+
function parseFileChangeChunk(chunk3) {
|
|
8672
|
+
if (!isRecord2(chunk3) || chunk3.type !== "data-file-changed") {
|
|
8170
8673
|
return null;
|
|
8171
8674
|
}
|
|
8172
|
-
const data =
|
|
8675
|
+
const data = chunk3.data;
|
|
8173
8676
|
if (!isRecord2(data)) {
|
|
8174
8677
|
return null;
|
|
8175
8678
|
}
|
|
@@ -8195,6 +8698,115 @@ function parseFileChangeChunk(chunk2) {
|
|
|
8195
8698
|
return change;
|
|
8196
8699
|
}
|
|
8197
8700
|
|
|
8701
|
+
// src/server/http/turnManager.ts
|
|
8702
|
+
function chunk2(data) {
|
|
8703
|
+
return data;
|
|
8704
|
+
}
|
|
8705
|
+
var TurnManager = class {
|
|
8706
|
+
buffers = new StreamBufferStore();
|
|
8707
|
+
activeAbortControllersBySession = /* @__PURE__ */ new Map();
|
|
8708
|
+
activeTurnBySession = /* @__PURE__ */ new Map();
|
|
8709
|
+
async startTurn(options) {
|
|
8710
|
+
const reserved = this.reserve(options.sessionId, options.turnId);
|
|
8711
|
+
if (!reserved) return { active: true };
|
|
8712
|
+
try {
|
|
8713
|
+
const runtime = await options.resolveRuntime();
|
|
8714
|
+
options.onSubmitted?.();
|
|
8715
|
+
const chunks = runtime.harness.sendMessage(options.input, {
|
|
8716
|
+
abortSignal: reserved.abortController.signal,
|
|
8717
|
+
workdir: runtime.workdir
|
|
8718
|
+
});
|
|
8719
|
+
this.pump(reserved, chunks, options);
|
|
8720
|
+
return { turnId: reserved.turnId, buffer: reserved.buffer };
|
|
8721
|
+
} catch (err) {
|
|
8722
|
+
this.cleanup(reserved);
|
|
8723
|
+
reserved.buffer.markComplete(() => this.buffers.evict(reserved.sessionId, reserved.turnId));
|
|
8724
|
+
throw err;
|
|
8725
|
+
}
|
|
8726
|
+
}
|
|
8727
|
+
abortTurn(sessionId, turnId) {
|
|
8728
|
+
const requestedTurnId = turnId ?? this.activeTurnBySession.get(sessionId);
|
|
8729
|
+
if (!requestedTurnId) return;
|
|
8730
|
+
this.activeAbortControllersBySession.get(sessionId)?.get(requestedTurnId)?.abort();
|
|
8731
|
+
}
|
|
8732
|
+
getActive(sessionId) {
|
|
8733
|
+
return this.buffers.getActive(sessionId);
|
|
8734
|
+
}
|
|
8735
|
+
reserve(sessionId, turnId) {
|
|
8736
|
+
if (this.activeTurnBySession.has(sessionId)) return null;
|
|
8737
|
+
const abortController = new AbortController();
|
|
8738
|
+
const sessionAbortControllers = this.activeAbortControllersBySession.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
8739
|
+
sessionAbortControllers.set(turnId, abortController);
|
|
8740
|
+
this.activeAbortControllersBySession.set(sessionId, sessionAbortControllers);
|
|
8741
|
+
this.activeTurnBySession.set(sessionId, turnId);
|
|
8742
|
+
return {
|
|
8743
|
+
sessionId,
|
|
8744
|
+
turnId,
|
|
8745
|
+
abortController,
|
|
8746
|
+
buffer: this.buffers.create(sessionId, turnId)
|
|
8747
|
+
};
|
|
8748
|
+
}
|
|
8749
|
+
pump(reserved, chunks, options) {
|
|
8750
|
+
void (async () => {
|
|
8751
|
+
let streamFailed = false;
|
|
8752
|
+
try {
|
|
8753
|
+
reserved.buffer.append(chunk2({ type: "data-turn-start", data: { turnId: reserved.turnId } }));
|
|
8754
|
+
for await (const rawChunk of chunks) {
|
|
8755
|
+
const nextChunk = rawChunk;
|
|
8756
|
+
const fileChange = parseFileChangeChunk(nextChunk);
|
|
8757
|
+
if (fileChange) {
|
|
8758
|
+
options.sessionChangesTracker?.record(reserved.sessionId, fileChange);
|
|
8759
|
+
}
|
|
8760
|
+
reserved.buffer.append(nextChunk);
|
|
8761
|
+
}
|
|
8762
|
+
} catch (err) {
|
|
8763
|
+
streamFailed = true;
|
|
8764
|
+
options.onStreamError?.(err);
|
|
8765
|
+
reserved.buffer.append({
|
|
8766
|
+
type: "error",
|
|
8767
|
+
errorText: "internal error"
|
|
8768
|
+
});
|
|
8769
|
+
} finally {
|
|
8770
|
+
this.cleanup(reserved);
|
|
8771
|
+
if (!streamFailed) options.onStreamComplete?.();
|
|
8772
|
+
reserved.buffer.markComplete(() => this.buffers.evict(reserved.sessionId, reserved.turnId));
|
|
8773
|
+
}
|
|
8774
|
+
})();
|
|
8775
|
+
}
|
|
8776
|
+
cleanup(reserved) {
|
|
8777
|
+
const sessionAbortControllers = this.activeAbortControllersBySession.get(reserved.sessionId);
|
|
8778
|
+
if (sessionAbortControllers?.get(reserved.turnId) === reserved.abortController) {
|
|
8779
|
+
sessionAbortControllers.delete(reserved.turnId);
|
|
8780
|
+
if (sessionAbortControllers.size === 0) this.activeAbortControllersBySession.delete(reserved.sessionId);
|
|
8781
|
+
}
|
|
8782
|
+
if (this.activeTurnBySession.get(reserved.sessionId) === reserved.turnId) {
|
|
8783
|
+
this.activeTurnBySession.delete(reserved.sessionId);
|
|
8784
|
+
}
|
|
8785
|
+
}
|
|
8786
|
+
};
|
|
8787
|
+
function createBufferedUiMessageStream(buffer, closeEmitter, cursor = -1) {
|
|
8788
|
+
return createUIMessageStream({
|
|
8789
|
+
async execute({ writer }) {
|
|
8790
|
+
const replayed = buffer.replay(cursor);
|
|
8791
|
+
for (const e of replayed) writer.write(e.chunk);
|
|
8792
|
+
if (buffer.complete) return;
|
|
8793
|
+
await new Promise((resolve11) => {
|
|
8794
|
+
const unsub = buffer.subscribe(
|
|
8795
|
+
(e) => writer.write(e.chunk),
|
|
8796
|
+
() => {
|
|
8797
|
+
unsub();
|
|
8798
|
+
resolve11();
|
|
8799
|
+
}
|
|
8800
|
+
);
|
|
8801
|
+
closeEmitter.on("close", () => {
|
|
8802
|
+
unsub();
|
|
8803
|
+
resolve11();
|
|
8804
|
+
});
|
|
8805
|
+
});
|
|
8806
|
+
}
|
|
8807
|
+
});
|
|
8808
|
+
}
|
|
8809
|
+
|
|
8198
8810
|
// src/server/harness/pi-coding-agent/projectPiDataMessages.ts
|
|
8199
8811
|
function projectPiDataMessages(messages) {
|
|
8200
8812
|
const dataParts = messages.flatMap((message) => message.parts ?? []).filter((part) => {
|
|
@@ -8268,7 +8880,8 @@ var chatBodySchema = z.object({
|
|
|
8268
8880
|
mediaType: z.string().optional(),
|
|
8269
8881
|
url: z.string()
|
|
8270
8882
|
})
|
|
8271
|
-
).max(20).optional()
|
|
8883
|
+
).max(20).optional(),
|
|
8884
|
+
clientTurnId: z.string().min(1).max(128).optional()
|
|
8272
8885
|
});
|
|
8273
8886
|
function addTelemetryProperty(properties, key, value) {
|
|
8274
8887
|
if (typeof value === "string" && value) properties[key] = value;
|
|
@@ -8282,7 +8895,7 @@ function chatRoutes(app, opts, done) {
|
|
|
8282
8895
|
const { sessionChangesTracker } = opts;
|
|
8283
8896
|
const telemetry = opts.telemetry ?? noopTelemetry;
|
|
8284
8897
|
const validateBody = createBodyValidator(chatBodySchema);
|
|
8285
|
-
const
|
|
8898
|
+
const turnManager = new TurnManager();
|
|
8286
8899
|
const lastFollowUpBySession = /* @__PURE__ */ new Map();
|
|
8287
8900
|
const cancelledFollowUpsBySession = /* @__PURE__ */ new Map();
|
|
8288
8901
|
const MAX_FOLLOWUP_CACHE = 5e3;
|
|
@@ -8319,12 +8932,18 @@ function chatRoutes(app, opts, done) {
|
|
|
8319
8932
|
}
|
|
8320
8933
|
throw new Error("chat route requires harness/workdir or getRuntime");
|
|
8321
8934
|
}
|
|
8935
|
+
async function resolveSessionStore(request) {
|
|
8936
|
+
if (opts.getSessionStore) return await opts.getSessionStore(request);
|
|
8937
|
+
if (opts.sessionStore) return opts.sessionStore;
|
|
8938
|
+
const runtime = await resolveRuntime(request);
|
|
8939
|
+
return runtime.harness.sessions;
|
|
8940
|
+
}
|
|
8322
8941
|
app.post(
|
|
8323
8942
|
"/api/v1/agent/chat",
|
|
8324
8943
|
{ preHandler: validateBody },
|
|
8325
8944
|
async (request, reply) => {
|
|
8326
|
-
const { sessionId, message, model, thinkingLevel, attachments } = request.body;
|
|
8327
|
-
const turnId = randomUUID3();
|
|
8945
|
+
const { sessionId, message, model, thinkingLevel, attachments, clientTurnId } = request.body;
|
|
8946
|
+
const turnId = clientTurnId ?? randomUUID3();
|
|
8328
8947
|
const startedAt = Date.now();
|
|
8329
8948
|
const telemetryProperties2 = {
|
|
8330
8949
|
sessionId,
|
|
@@ -8337,83 +8956,57 @@ function chatRoutes(app, opts, done) {
|
|
|
8337
8956
|
name: "agent.chat.started",
|
|
8338
8957
|
properties: telemetryProperties2
|
|
8339
8958
|
});
|
|
8340
|
-
const abortController = new AbortController();
|
|
8341
|
-
let streamStarted = false;
|
|
8342
|
-
let streamCompleted = false;
|
|
8343
|
-
reply.raw.on("close", () => {
|
|
8344
|
-
if (streamStarted && !streamCompleted && !abortController.signal.aborted) {
|
|
8345
|
-
abortController.abort();
|
|
8346
|
-
}
|
|
8347
|
-
});
|
|
8348
|
-
const buf = buffers.create(sessionId, turnId);
|
|
8349
8959
|
try {
|
|
8350
|
-
const
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
sessionChangesTracker?.record(sessionId, fileChange);
|
|
8372
|
-
}
|
|
8373
|
-
buf.append(c);
|
|
8374
|
-
writer.write(c);
|
|
8960
|
+
const startedTurn = await turnManager.startTurn({
|
|
8961
|
+
sessionId,
|
|
8962
|
+
turnId,
|
|
8963
|
+
input: { sessionId, message, model, thinkingLevel, attachments },
|
|
8964
|
+
resolveRuntime: () => resolveRuntime(request),
|
|
8965
|
+
sessionChangesTracker,
|
|
8966
|
+
onSubmitted: () => {
|
|
8967
|
+
safeCapture(telemetry, {
|
|
8968
|
+
name: "agent.chat.message.submitted",
|
|
8969
|
+
properties: telemetryProperties2
|
|
8970
|
+
});
|
|
8971
|
+
},
|
|
8972
|
+
onStreamError: (err) => {
|
|
8973
|
+
request.log.error({ err, sessionId }, "[chat] stream error");
|
|
8974
|
+
safeCapture(telemetry, {
|
|
8975
|
+
name: "agent.chat.failed",
|
|
8976
|
+
properties: {
|
|
8977
|
+
...telemetryProperties2,
|
|
8978
|
+
status: "error",
|
|
8979
|
+
durationMs: Date.now() - startedAt,
|
|
8980
|
+
errorCode: ErrorCode.enum.INTERNAL_ERROR
|
|
8375
8981
|
}
|
|
8376
|
-
}
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
errorCode: ErrorCode.enum.INTERNAL_ERROR
|
|
8386
|
-
}
|
|
8387
|
-
});
|
|
8388
|
-
const errChunk = {
|
|
8389
|
-
type: "error",
|
|
8390
|
-
errorText: "internal error"
|
|
8391
|
-
};
|
|
8392
|
-
buf.append(errChunk);
|
|
8393
|
-
writer.write(errChunk);
|
|
8394
|
-
} finally {
|
|
8395
|
-
streamCompleted = true;
|
|
8396
|
-
if (!streamFailed) {
|
|
8397
|
-
safeCapture(telemetry, {
|
|
8398
|
-
name: "agent.chat.completed",
|
|
8399
|
-
properties: {
|
|
8400
|
-
...telemetryProperties2,
|
|
8401
|
-
status: "ok",
|
|
8402
|
-
durationMs: Date.now() - startedAt
|
|
8403
|
-
}
|
|
8404
|
-
});
|
|
8982
|
+
});
|
|
8983
|
+
},
|
|
8984
|
+
onStreamComplete: () => {
|
|
8985
|
+
safeCapture(telemetry, {
|
|
8986
|
+
name: "agent.chat.completed",
|
|
8987
|
+
properties: {
|
|
8988
|
+
...telemetryProperties2,
|
|
8989
|
+
status: "ok",
|
|
8990
|
+
durationMs: Date.now() - startedAt
|
|
8405
8991
|
}
|
|
8406
|
-
|
|
8407
|
-
}
|
|
8992
|
+
});
|
|
8408
8993
|
}
|
|
8409
8994
|
});
|
|
8410
|
-
|
|
8995
|
+
if ("active" in startedTurn) {
|
|
8996
|
+
return reply.code(409).send({
|
|
8997
|
+
error: {
|
|
8998
|
+
code: ERROR_CODE_CONFLICT,
|
|
8999
|
+
message: "turn_already_active"
|
|
9000
|
+
}
|
|
9001
|
+
});
|
|
9002
|
+
}
|
|
9003
|
+
const stream = createBufferedUiMessageStream(startedTurn.buffer, request.raw);
|
|
8411
9004
|
reply.hijack();
|
|
8412
9005
|
pipeUIMessageStreamToResponse({
|
|
8413
9006
|
response: reply.raw,
|
|
8414
9007
|
stream,
|
|
8415
9008
|
headers: {
|
|
8416
|
-
"X-Turn-Id": turnId,
|
|
9009
|
+
"X-Turn-Id": startedTurn.turnId,
|
|
8417
9010
|
"X-Accel-Buffering": "no",
|
|
8418
9011
|
"Cache-Control": "no-cache, no-transform"
|
|
8419
9012
|
}
|
|
@@ -8431,8 +9024,6 @@ function chatRoutes(app, opts, done) {
|
|
|
8431
9024
|
errorCode: safeTelemetryErrorCode(err?.code)
|
|
8432
9025
|
}
|
|
8433
9026
|
});
|
|
8434
|
-
buf.markComplete(() => buffers.evict(sessionId, turnId));
|
|
8435
|
-
if (streamStarted) return;
|
|
8436
9027
|
const statusCode = err?.statusCode;
|
|
8437
9028
|
const stableCode = err?.code;
|
|
8438
9029
|
if (typeof statusCode === "number" && statusCode >= 400 && statusCode < 600) {
|
|
@@ -8464,7 +9055,7 @@ function chatRoutes(app, opts, done) {
|
|
|
8464
9055
|
}
|
|
8465
9056
|
});
|
|
8466
9057
|
}
|
|
8467
|
-
const active =
|
|
9058
|
+
const active = turnManager.getActive(sessionId);
|
|
8468
9059
|
if (!active) {
|
|
8469
9060
|
return reply.code(204).send();
|
|
8470
9061
|
}
|
|
@@ -8481,26 +9072,7 @@ function chatRoutes(app, opts, done) {
|
|
|
8481
9072
|
{ sessionId, cursor, bufHigh: buf.highIdx },
|
|
8482
9073
|
"[resume] replaying"
|
|
8483
9074
|
);
|
|
8484
|
-
const stream =
|
|
8485
|
-
async execute({ writer }) {
|
|
8486
|
-
const replayed = buf.replay(cursor);
|
|
8487
|
-
for (const e of replayed) writer.write(e.chunk);
|
|
8488
|
-
if (buf.complete) return;
|
|
8489
|
-
await new Promise((resolve10) => {
|
|
8490
|
-
const unsub = buf.subscribe(
|
|
8491
|
-
(e) => writer.write(e.chunk),
|
|
8492
|
-
() => {
|
|
8493
|
-
unsub();
|
|
8494
|
-
resolve10();
|
|
8495
|
-
}
|
|
8496
|
-
);
|
|
8497
|
-
request.raw.on("close", () => {
|
|
8498
|
-
unsub();
|
|
8499
|
-
resolve10();
|
|
8500
|
-
});
|
|
8501
|
-
});
|
|
8502
|
-
}
|
|
8503
|
-
});
|
|
9075
|
+
const stream = createBufferedUiMessageStream(buf, request.raw, cursor);
|
|
8504
9076
|
reply.hijack();
|
|
8505
9077
|
pipeUIMessageStreamToResponse({
|
|
8506
9078
|
response: reply.raw,
|
|
@@ -8521,8 +9093,8 @@ function chatRoutes(app, opts, done) {
|
|
|
8521
9093
|
workspaceId: request.workspaceContext?.workspaceId ?? "default"
|
|
8522
9094
|
};
|
|
8523
9095
|
try {
|
|
8524
|
-
const
|
|
8525
|
-
const detail = await
|
|
9096
|
+
const store = await resolveSessionStore(request);
|
|
9097
|
+
const detail = await store.load(ctx, sessionId);
|
|
8526
9098
|
const messages = Array.isArray(detail?.messages) ? detail.messages : [];
|
|
8527
9099
|
return reply.code(200).send({ messages });
|
|
8528
9100
|
} catch {
|
|
@@ -8530,6 +9102,16 @@ function chatRoutes(app, opts, done) {
|
|
|
8530
9102
|
}
|
|
8531
9103
|
}
|
|
8532
9104
|
);
|
|
9105
|
+
app.delete(
|
|
9106
|
+
"/api/v1/agent/chat/:sessionId/turn",
|
|
9107
|
+
async (request, reply) => {
|
|
9108
|
+
const { sessionId } = request.params;
|
|
9109
|
+
const query = request.query;
|
|
9110
|
+
const requestedTurnId = typeof query.turnId === "string" && query.turnId.length > 0 ? query.turnId : void 0;
|
|
9111
|
+
turnManager.abortTurn(sessionId, requestedTurnId);
|
|
9112
|
+
return reply.code(204).send();
|
|
9113
|
+
}
|
|
9114
|
+
);
|
|
8533
9115
|
app.post(
|
|
8534
9116
|
"/api/v1/agent/chat/:sessionId/followup",
|
|
8535
9117
|
async (request, reply) => {
|
|
@@ -8619,9 +9201,9 @@ function chatRoutes(app, opts, done) {
|
|
|
8619
9201
|
workspaceId: request.workspaceContext?.workspaceId ?? "default"
|
|
8620
9202
|
};
|
|
8621
9203
|
try {
|
|
8622
|
-
const
|
|
8623
|
-
if (
|
|
8624
|
-
await
|
|
9204
|
+
const store = await resolveSessionStore(request);
|
|
9205
|
+
if (store.saveMessages) {
|
|
9206
|
+
await store.saveMessages(ctx, sessionId, projectPiDataMessages(body.messages));
|
|
8625
9207
|
}
|
|
8626
9208
|
return reply.code(204).send();
|
|
8627
9209
|
} catch {
|
|
@@ -8634,41 +9216,23 @@ function chatRoutes(app, opts, done) {
|
|
|
8634
9216
|
|
|
8635
9217
|
// src/server/http/routes/models.ts
|
|
8636
9218
|
import { AuthStorage as AuthStorage2, ModelRegistry as ModelRegistry2 } from "@mariozechner/pi-coding-agent";
|
|
8637
|
-
var AUTH_CHECK_TTL_MS = 6e4;
|
|
8638
9219
|
function modelsRoutes(app, _opts, done) {
|
|
8639
9220
|
const authStorage = AuthStorage2.create();
|
|
8640
9221
|
const registry = ModelRegistry2.create(authStorage);
|
|
8641
9222
|
registerConfiguredModelProviders(registry);
|
|
8642
|
-
const providerAuthCache = /* @__PURE__ */ new Map();
|
|
8643
|
-
async function providerHasResolvableAuth(provider) {
|
|
8644
|
-
const now = Date.now();
|
|
8645
|
-
const cached = providerAuthCache.get(provider);
|
|
8646
|
-
if (cached && cached.expiresAt > now) return cached.available;
|
|
8647
|
-
let available = false;
|
|
8648
|
-
try {
|
|
8649
|
-
const model = registry.getAvailable().find((candidate) => candidate.provider === provider);
|
|
8650
|
-
const auth = model ? await registry.getApiKeyAndHeaders(model) : void 0;
|
|
8651
|
-
available = auth?.ok === true && (typeof auth.apiKey === "string" && auth.apiKey.trim().length > 0 || auth.headers !== void 0);
|
|
8652
|
-
} catch {
|
|
8653
|
-
available = false;
|
|
8654
|
-
}
|
|
8655
|
-
providerAuthCache.set(provider, { available, expiresAt: now + AUTH_CHECK_TTL_MS });
|
|
8656
|
-
return available;
|
|
8657
|
-
}
|
|
8658
9223
|
app.get("/api/v1/agent/models", async (_request, reply) => {
|
|
8659
9224
|
const availableModels = registry.getAvailable();
|
|
8660
9225
|
const availableSet = new Set(
|
|
8661
9226
|
availableModels.map((m) => `${m.provider}:${m.id}`)
|
|
8662
9227
|
);
|
|
8663
|
-
const providerAvailability = /* @__PURE__ */ new Map();
|
|
8664
|
-
await Promise.all([...new Set(availableModels.map((m) => m.provider))].map(async (provider) => {
|
|
8665
|
-
providerAvailability.set(provider, await providerHasResolvableAuth(provider));
|
|
8666
|
-
}));
|
|
8667
9228
|
const models = registry.getAll().map((m) => ({
|
|
8668
9229
|
provider: m.provider,
|
|
8669
9230
|
id: m.id,
|
|
8670
9231
|
label: m.label ?? m.id,
|
|
8671
|
-
|
|
9232
|
+
// Keep this endpoint cheap: it is fetched on chat mount, so it must never
|
|
9233
|
+
// block workspace load on deep provider auth resolution. ModelRegistry's
|
|
9234
|
+
// available set is already derived from configured auth sources.
|
|
9235
|
+
available: availableSet.has(`${m.provider}:${m.id}`)
|
|
8672
9236
|
}));
|
|
8673
9237
|
models.sort((a, b) => {
|
|
8674
9238
|
if (a.available !== b.available) return a.available ? -1 : 1;
|
|
@@ -8746,6 +9310,8 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
8746
9310
|
import { z as z2 } from "zod";
|
|
8747
9311
|
var DEFAULT_SESSION_TITLE2 = "New session";
|
|
8748
9312
|
var DEFAULT_WORKSPACE_ID2 = "default";
|
|
9313
|
+
var DEFAULT_SESSION_LIST_LIMIT = 50;
|
|
9314
|
+
var MAX_SESSION_LIST_LIMIT = 100;
|
|
8749
9315
|
var MAX_ANALYSIS_TRANSCRIPT_CHARS = 12e4;
|
|
8750
9316
|
var createSessionBodySchema = z2.object({
|
|
8751
9317
|
title: z2.string().min(1).max(200).optional()
|
|
@@ -8764,8 +9330,11 @@ var SessionNotFoundError = class extends Error {
|
|
|
8764
9330
|
};
|
|
8765
9331
|
var InMemorySessionStore = class {
|
|
8766
9332
|
sessions = /* @__PURE__ */ new Map();
|
|
8767
|
-
async list(ctx) {
|
|
8768
|
-
|
|
9333
|
+
async list(ctx, options) {
|
|
9334
|
+
const offset = options?.offset ?? 0;
|
|
9335
|
+
const limit = options?.limit;
|
|
9336
|
+
const summaries = Array.from(this.sessions.values()).filter((session) => session.workspaceId === ctx.workspaceId).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).map(toSummary);
|
|
9337
|
+
return limit === void 0 ? summaries.slice(offset) : summaries.slice(offset, offset + limit);
|
|
8769
9338
|
}
|
|
8770
9339
|
async create(ctx, init) {
|
|
8771
9340
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -8813,6 +9382,18 @@ function toSummary(session) {
|
|
|
8813
9382
|
turnCount: session.messages.filter((message) => message.role === "user").length
|
|
8814
9383
|
};
|
|
8815
9384
|
}
|
|
9385
|
+
function sessionListOptions(request) {
|
|
9386
|
+
const query = request.query;
|
|
9387
|
+
return {
|
|
9388
|
+
limit: boundedInteger(query.limit, DEFAULT_SESSION_LIST_LIMIT, 1, MAX_SESSION_LIST_LIMIT),
|
|
9389
|
+
offset: boundedInteger(query.offset, 0, 0, Number.MAX_SAFE_INTEGER)
|
|
9390
|
+
};
|
|
9391
|
+
}
|
|
9392
|
+
function boundedInteger(value, fallback, min, max) {
|
|
9393
|
+
const parsed = typeof value === "string" ? Number.parseInt(value, 10) : NaN;
|
|
9394
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
9395
|
+
return Math.min(max, Math.max(min, parsed));
|
|
9396
|
+
}
|
|
8816
9397
|
function getSessionCtx(request) {
|
|
8817
9398
|
const workspaceContext = request.workspaceContext;
|
|
8818
9399
|
return {
|
|
@@ -8981,8 +9562,8 @@ function buildAnalysisPrompt(session, transcript, instructions) {
|
|
|
8981
9562
|
].filter(Boolean).join("\n");
|
|
8982
9563
|
}
|
|
8983
9564
|
function analysisTextFromChunks(chunks) {
|
|
8984
|
-
return chunks.map((
|
|
8985
|
-
const record =
|
|
9565
|
+
return chunks.map((chunk3) => {
|
|
9566
|
+
const record = chunk3;
|
|
8986
9567
|
return record.type === "text-delta" && typeof record.delta === "string" ? record.delta : "";
|
|
8987
9568
|
}).join("").trim();
|
|
8988
9569
|
}
|
|
@@ -9002,7 +9583,7 @@ function sessionRoutes(app, opts, done) {
|
|
|
9002
9583
|
app.get("/api/v1/agent/sessions", async (request, reply) => {
|
|
9003
9584
|
try {
|
|
9004
9585
|
const store = await resolveSessionStore(request);
|
|
9005
|
-
return await store.list(getSessionCtx(request));
|
|
9586
|
+
return await store.list(getSessionCtx(request), sessionListOptions(request));
|
|
9006
9587
|
} catch (err) {
|
|
9007
9588
|
return classifySessionError(err, reply);
|
|
9008
9589
|
}
|
|
@@ -9112,14 +9693,14 @@ function sessionRoutes(app, opts, done) {
|
|
|
9112
9693
|
}
|
|
9113
9694
|
const abortController = new AbortController();
|
|
9114
9695
|
const chunks = [];
|
|
9115
|
-
for await (const
|
|
9696
|
+
for await (const chunk3 of runtime.harness.sendMessage(
|
|
9116
9697
|
{ sessionId: analysisSession.id, message: prompt },
|
|
9117
9698
|
{
|
|
9118
9699
|
abortSignal: abortController.signal,
|
|
9119
9700
|
workdir: runtime.workdir
|
|
9120
9701
|
}
|
|
9121
9702
|
)) {
|
|
9122
|
-
chunks.push(
|
|
9703
|
+
chunks.push(chunk3);
|
|
9123
9704
|
}
|
|
9124
9705
|
return {
|
|
9125
9706
|
sourceSession: {
|
|
@@ -9249,12 +9830,35 @@ function readyStatusRoutes(app, opts, done) {
|
|
|
9249
9830
|
app.get("/api/v1/ready-status", async (request, reply) => {
|
|
9250
9831
|
const tracker = opts.getTracker ? await opts.getTracker(request) : opts.tracker;
|
|
9251
9832
|
if (!tracker) throw new Error("ready-status route requires tracker or getTracker");
|
|
9833
|
+
const initial = tracker.getReadiness();
|
|
9834
|
+
const initialEvent = {
|
|
9835
|
+
state: tracker.state,
|
|
9836
|
+
sandboxReady: initial.sandboxReady,
|
|
9837
|
+
harnessReady: initial.harnessReady,
|
|
9838
|
+
capabilities: initial.capabilities,
|
|
9839
|
+
message: initial.degradedReason,
|
|
9840
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9841
|
+
};
|
|
9842
|
+
const initialRuntimePending = initialEvent.capabilities.runtimeDependencies.state === "preparing";
|
|
9843
|
+
if (initialEvent.state === "degraded" || initialEvent.state === "ready" && !initialRuntimePending) {
|
|
9844
|
+
return reply.type("text/event-stream").headers({ "Cache-Control": "no-cache" }).send(`:
|
|
9845
|
+
|
|
9846
|
+
event: status
|
|
9847
|
+
data: ${JSON.stringify(initialEvent)}
|
|
9848
|
+
|
|
9849
|
+
`);
|
|
9850
|
+
}
|
|
9252
9851
|
reply.raw.writeHead(200, {
|
|
9253
9852
|
"Content-Type": "text/event-stream",
|
|
9254
9853
|
"Cache-Control": "no-cache",
|
|
9255
9854
|
Connection: "keep-alive"
|
|
9256
9855
|
});
|
|
9257
|
-
reply.raw.write(
|
|
9856
|
+
reply.raw.write(`:
|
|
9857
|
+
|
|
9858
|
+
event: status
|
|
9859
|
+
data: ${JSON.stringify(initialEvent)}
|
|
9860
|
+
|
|
9861
|
+
`);
|
|
9258
9862
|
let closed = false;
|
|
9259
9863
|
let unsubscribe = null;
|
|
9260
9864
|
const closeStream = () => {
|
|
@@ -9263,16 +9867,19 @@ function readyStatusRoutes(app, opts, done) {
|
|
|
9263
9867
|
unsubscribe?.();
|
|
9264
9868
|
reply.raw.end();
|
|
9265
9869
|
};
|
|
9870
|
+
request.raw.on("close", closeStream);
|
|
9871
|
+
reply.hijack();
|
|
9266
9872
|
unsubscribe = tracker.subscribe((event) => {
|
|
9267
9873
|
if (closed) return;
|
|
9268
9874
|
reply.raw.write(`event: status
|
|
9269
9875
|
data: ${JSON.stringify(event)}
|
|
9270
9876
|
|
|
9271
9877
|
`);
|
|
9272
|
-
|
|
9878
|
+
const runtimePending = event.capabilities.runtimeDependencies.state === "preparing";
|
|
9879
|
+
if (event.state === "degraded" || event.state === "ready" && !runtimePending) {
|
|
9880
|
+
queueMicrotask(closeStream);
|
|
9881
|
+
}
|
|
9273
9882
|
});
|
|
9274
|
-
request.raw.on("close", closeStream);
|
|
9275
|
-
reply.hijack();
|
|
9276
9883
|
return reply;
|
|
9277
9884
|
});
|
|
9278
9885
|
done();
|
|
@@ -9370,14 +9977,26 @@ function searchRoutes(app, opts, done) {
|
|
|
9370
9977
|
}
|
|
9371
9978
|
|
|
9372
9979
|
// src/server/sandbox/vercel-sandbox/readyStatus.ts
|
|
9980
|
+
function defaultCapabilities(sandboxReady, harnessReady) {
|
|
9981
|
+
return {
|
|
9982
|
+
chat: { state: harnessReady ? "ready" : "preparing" },
|
|
9983
|
+
workspace: { state: sandboxReady ? "ready" : "preparing" },
|
|
9984
|
+
runtimeDependencies: { state: "ready" }
|
|
9985
|
+
};
|
|
9986
|
+
}
|
|
9373
9987
|
var ReadyStatusTracker = class {
|
|
9374
9988
|
_sandboxReady;
|
|
9375
9989
|
_harnessReady;
|
|
9376
9990
|
_degradedReason;
|
|
9991
|
+
_capabilities;
|
|
9377
9992
|
subscribers = /* @__PURE__ */ new Set();
|
|
9378
9993
|
constructor(opts) {
|
|
9379
9994
|
this._sandboxReady = opts?.sandboxReady ?? false;
|
|
9380
9995
|
this._harnessReady = opts?.harnessReady ?? false;
|
|
9996
|
+
this._capabilities = {
|
|
9997
|
+
...defaultCapabilities(this._sandboxReady, this._harnessReady),
|
|
9998
|
+
...opts?.capabilities ?? {}
|
|
9999
|
+
};
|
|
9381
10000
|
}
|
|
9382
10001
|
get state() {
|
|
9383
10002
|
if (this._degradedReason) return "degraded";
|
|
@@ -9390,23 +10009,52 @@ var ReadyStatusTracker = class {
|
|
|
9390
10009
|
return {
|
|
9391
10010
|
sandboxReady: this._sandboxReady,
|
|
9392
10011
|
harnessReady: this._harnessReady,
|
|
10012
|
+
capabilities: this.getCapabilities(),
|
|
9393
10013
|
degradedReason: this._degradedReason
|
|
9394
10014
|
};
|
|
9395
10015
|
}
|
|
10016
|
+
getCapabilities() {
|
|
10017
|
+
return {
|
|
10018
|
+
chat: { ...this._capabilities.chat },
|
|
10019
|
+
workspace: { ...this._capabilities.workspace },
|
|
10020
|
+
runtimeDependencies: { ...this._capabilities.runtimeDependencies }
|
|
10021
|
+
};
|
|
10022
|
+
}
|
|
10023
|
+
updateCapability(name, detail) {
|
|
10024
|
+
this._capabilities = {
|
|
10025
|
+
...this._capabilities,
|
|
10026
|
+
[name]: { ...detail }
|
|
10027
|
+
};
|
|
10028
|
+
this.emit();
|
|
10029
|
+
}
|
|
10030
|
+
updateRuntimeDependencies(detail) {
|
|
10031
|
+
this.updateCapability("runtimeDependencies", detail);
|
|
10032
|
+
}
|
|
9396
10033
|
markSandboxReady() {
|
|
9397
10034
|
if (this._sandboxReady) return;
|
|
9398
10035
|
this._sandboxReady = true;
|
|
10036
|
+
if (this._capabilities.workspace.state === "preparing") {
|
|
10037
|
+
this._capabilities.workspace = { state: "ready" };
|
|
10038
|
+
}
|
|
9399
10039
|
this.emit();
|
|
9400
10040
|
}
|
|
9401
10041
|
markHarnessReady() {
|
|
9402
10042
|
if (this._harnessReady) return;
|
|
9403
10043
|
this._harnessReady = true;
|
|
10044
|
+
if (this._capabilities.chat.state === "preparing") {
|
|
10045
|
+
this._capabilities.chat = { state: "ready" };
|
|
10046
|
+
}
|
|
9404
10047
|
this.emit();
|
|
9405
10048
|
}
|
|
9406
10049
|
markDegraded(reason) {
|
|
9407
10050
|
this._degradedReason = reason;
|
|
9408
10051
|
this.emit();
|
|
9409
10052
|
}
|
|
10053
|
+
clearDegraded() {
|
|
10054
|
+
if (!this._degradedReason) return;
|
|
10055
|
+
this._degradedReason = void 0;
|
|
10056
|
+
this.emit();
|
|
10057
|
+
}
|
|
9410
10058
|
subscribe(handler) {
|
|
9411
10059
|
this.subscribers.add(handler);
|
|
9412
10060
|
handler(this.snapshot());
|
|
@@ -9423,6 +10071,7 @@ var ReadyStatusTracker = class {
|
|
|
9423
10071
|
state: this.state,
|
|
9424
10072
|
sandboxReady: this._sandboxReady,
|
|
9425
10073
|
harnessReady: this._harnessReady,
|
|
10074
|
+
capabilities: this.getCapabilities(),
|
|
9426
10075
|
message: this._degradedReason,
|
|
9427
10076
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9428
10077
|
};
|
|
@@ -9518,6 +10167,7 @@ async function createAgentApp(opts = {}) {
|
|
|
9518
10167
|
await app.register(chatRoutes, {
|
|
9519
10168
|
harness,
|
|
9520
10169
|
workdir: runtimeBundle.workspace.root,
|
|
10170
|
+
sessionStore: harness.sessions,
|
|
9521
10171
|
sessionChangesTracker,
|
|
9522
10172
|
telemetry: opts.telemetry
|
|
9523
10173
|
});
|
|
@@ -9578,36 +10228,6 @@ function applyCspHeaders(response, opts = {}) {
|
|
|
9578
10228
|
import { basename as basename3 } from "path";
|
|
9579
10229
|
import { AuthStorage as AuthStorage3, ModelRegistry as ModelRegistry3 } from "@mariozechner/pi-coding-agent";
|
|
9580
10230
|
|
|
9581
|
-
// src/server/catalog/toolReadiness.ts
|
|
9582
|
-
var WORKSPACE_PREPARING_MESSAGE = "Workspace is still preparing. Try again in a moment.";
|
|
9583
|
-
function workspaceNotReadyToolResult(requirement) {
|
|
9584
|
-
return {
|
|
9585
|
-
content: [{ type: "text", text: WORKSPACE_PREPARING_MESSAGE }],
|
|
9586
|
-
isError: true,
|
|
9587
|
-
details: {
|
|
9588
|
-
code: ErrorCode.enum.WORKSPACE_NOT_READY,
|
|
9589
|
-
retryable: true,
|
|
9590
|
-
requirement
|
|
9591
|
-
}
|
|
9592
|
-
};
|
|
9593
|
-
}
|
|
9594
|
-
function withReadinessRequirements(tool, readinessRequirements) {
|
|
9595
|
-
if (tool.readinessRequirements === readinessRequirements) return tool;
|
|
9596
|
-
return { ...tool, readinessRequirements };
|
|
9597
|
-
}
|
|
9598
|
-
function wrapToolForReadiness(tool, checkReadiness) {
|
|
9599
|
-
if (!checkReadiness || !tool.readinessRequirements || tool.readinessRequirements.length === 0) return tool;
|
|
9600
|
-
return {
|
|
9601
|
-
...tool,
|
|
9602
|
-
async execute(params, ctx) {
|
|
9603
|
-
for (const requirement of tool.readinessRequirements ?? []) {
|
|
9604
|
-
if (!checkReadiness(requirement, tool)) return workspaceNotReadyToolResult(requirement);
|
|
9605
|
-
}
|
|
9606
|
-
return await tool.execute(params, ctx);
|
|
9607
|
-
}
|
|
9608
|
-
};
|
|
9609
|
-
}
|
|
9610
|
-
|
|
9611
10231
|
// src/server/catalog/mergeTools.ts
|
|
9612
10232
|
function setLastRegistered(merged, tool) {
|
|
9613
10233
|
merged.delete(tool.name);
|
|
@@ -9642,11 +10262,11 @@ function mergeTools(options) {
|
|
|
9642
10262
|
|
|
9643
10263
|
// src/server/tools/upload/index.ts
|
|
9644
10264
|
import { readFile as readFile12 } from "fs/promises";
|
|
9645
|
-
import { extname as
|
|
10265
|
+
import { extname as extname5, join as join13 } from "path";
|
|
9646
10266
|
var DEFAULT_UPLOAD_DIR = "assets/images";
|
|
9647
10267
|
var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
|
|
9648
10268
|
function contentTypeFromExt(path4) {
|
|
9649
|
-
switch (
|
|
10269
|
+
switch (extname5(path4).toLowerCase()) {
|
|
9650
10270
|
case ".jpg":
|
|
9651
10271
|
case ".jpeg":
|
|
9652
10272
|
return "image/jpeg";
|
|
@@ -9665,7 +10285,7 @@ function contentTypeFromExt(path4) {
|
|
|
9665
10285
|
}
|
|
9666
10286
|
}
|
|
9667
10287
|
function extForUpload2(filename, contentType) {
|
|
9668
|
-
const fromName =
|
|
10288
|
+
const fromName = extname5(filename).toLowerCase().replace(/^\./, "");
|
|
9669
10289
|
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
9670
10290
|
if (contentType === "image/jpeg") return "jpg";
|
|
9671
10291
|
if (contentType === "image/png") return "png";
|
|
@@ -9831,6 +10451,29 @@ function createRuntimeProvisioningFailedError(workspaceId, cause) {
|
|
|
9831
10451
|
}
|
|
9832
10452
|
);
|
|
9833
10453
|
}
|
|
10454
|
+
function isRuntimeReadinessRequirement(requirement) {
|
|
10455
|
+
return requirement === "runtime-dependencies" || requirement.startsWith("runtime:");
|
|
10456
|
+
}
|
|
10457
|
+
function causeCodeFrom(error) {
|
|
10458
|
+
const code = error?.code;
|
|
10459
|
+
return typeof code === "string" ? code : void 0;
|
|
10460
|
+
}
|
|
10461
|
+
function createRuntimeReadinessCheck(workspaceId, getRuntimeDependencies) {
|
|
10462
|
+
return (requirement) => {
|
|
10463
|
+
if (!isRuntimeReadinessRequirement(requirement)) return true;
|
|
10464
|
+
const runtimeDependencies = getRuntimeDependencies();
|
|
10465
|
+
if (runtimeDependencies.state === "ready" || runtimeDependencies.state === "not-started") return true;
|
|
10466
|
+
return {
|
|
10467
|
+
ready: false,
|
|
10468
|
+
state: runtimeDependencies.state,
|
|
10469
|
+
errorCode: runtimeDependencies.errorCode,
|
|
10470
|
+
causeCode: runtimeDependencies.causeCode,
|
|
10471
|
+
message: runtimeDependencies.message,
|
|
10472
|
+
workspaceId,
|
|
10473
|
+
retryable: runtimeDependencies.retryable ?? true
|
|
10474
|
+
};
|
|
10475
|
+
};
|
|
10476
|
+
}
|
|
9834
10477
|
var registerAgentRoutes = async (app, opts) => {
|
|
9835
10478
|
const workspaceRoot = opts.workspaceRoot ?? process.cwd();
|
|
9836
10479
|
const sessionId = opts.sessionId ?? DEFAULT_WORKSPACE_ID3;
|
|
@@ -9926,14 +10569,99 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9926
10569
|
requestId: request?.id,
|
|
9927
10570
|
telemetry: opts.telemetry
|
|
9928
10571
|
};
|
|
9929
|
-
let runtimeProvisioning
|
|
10572
|
+
let runtimeProvisioning;
|
|
10573
|
+
let runtimeDependencies = hasRuntimeProvisioningInput ? {
|
|
10574
|
+
state: "preparing",
|
|
10575
|
+
requirement: "runtime-dependencies",
|
|
10576
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10577
|
+
retryable: true
|
|
10578
|
+
} : { state: "ready" };
|
|
10579
|
+
let provisioningGeneration = 0;
|
|
9930
10580
|
const runtimeBundle = await modeAdapter.create(modeCtx);
|
|
10581
|
+
const readyTracker = new ReadyStatusTracker({
|
|
10582
|
+
sandboxReady: resolvedMode !== "vercel-sandbox",
|
|
10583
|
+
harnessReady: false,
|
|
10584
|
+
capabilities: {
|
|
10585
|
+
chat: { state: "preparing" },
|
|
10586
|
+
workspace: { state: resolvedMode !== "vercel-sandbox" ? "ready" : "preparing" },
|
|
10587
|
+
runtimeDependencies
|
|
10588
|
+
}
|
|
10589
|
+
});
|
|
10590
|
+
if (resolvedMode === "vercel-sandbox") {
|
|
10591
|
+
queueMicrotask(() => readyTracker.markSandboxReady());
|
|
10592
|
+
}
|
|
10593
|
+
let binding;
|
|
10594
|
+
const updateRuntimeDependencies = (next) => {
|
|
10595
|
+
runtimeDependencies = next;
|
|
10596
|
+
if (binding) binding.runtimeDependencies = next;
|
|
10597
|
+
readyTracker.updateRuntimeDependencies(next);
|
|
10598
|
+
};
|
|
10599
|
+
const startRuntimeProvisioning = (provisionRequest) => {
|
|
10600
|
+
if (!hasRuntimeProvisioningInput) return void 0;
|
|
10601
|
+
if (binding?.runtimeProvisioningTask && runtimeDependencies.state === "preparing") {
|
|
10602
|
+
return binding.runtimeProvisioningTask;
|
|
10603
|
+
}
|
|
10604
|
+
const generation = ++provisioningGeneration;
|
|
10605
|
+
readyTracker.clearDegraded();
|
|
10606
|
+
updateRuntimeDependencies({
|
|
10607
|
+
state: "preparing",
|
|
10608
|
+
requirement: "runtime-dependencies",
|
|
10609
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10610
|
+
retryable: true
|
|
10611
|
+
});
|
|
10612
|
+
const task = runRuntimeProvisioning(workspaceId, scope, provisionRequest).then(
|
|
10613
|
+
async (result) => {
|
|
10614
|
+
if (generation !== provisioningGeneration) return result;
|
|
10615
|
+
runtimeProvisioning = result;
|
|
10616
|
+
if (binding) binding.runtimeProvisioning = result;
|
|
10617
|
+
if (binding?.harness.reloadSession) {
|
|
10618
|
+
try {
|
|
10619
|
+
const sessions = await binding.harness.sessions.list({ workspaceId });
|
|
10620
|
+
await Promise.allSettled(
|
|
10621
|
+
sessions.map((session) => binding?.harness.reloadSession?.(session.id))
|
|
10622
|
+
);
|
|
10623
|
+
} catch (error) {
|
|
10624
|
+
app.log.warn({ err: error, workspaceId }, "[agent] failed to refresh harness sessions after runtime provisioning");
|
|
10625
|
+
}
|
|
10626
|
+
}
|
|
10627
|
+
updateRuntimeDependencies({
|
|
10628
|
+
state: "ready",
|
|
10629
|
+
requirement: "runtime-dependencies",
|
|
10630
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10631
|
+
retryable: true
|
|
10632
|
+
});
|
|
10633
|
+
return result;
|
|
10634
|
+
},
|
|
10635
|
+
(error) => {
|
|
10636
|
+
if (generation !== provisioningGeneration) throw error;
|
|
10637
|
+
const causeCode = causeCodeFrom(error);
|
|
10638
|
+
updateRuntimeDependencies({
|
|
10639
|
+
state: "failed",
|
|
10640
|
+
requirement: "runtime-dependencies",
|
|
10641
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10642
|
+
errorCode: ErrorCode.enum.RUNTIME_PROVISIONING_FAILED,
|
|
10643
|
+
...causeCode ? { causeCode } : {},
|
|
10644
|
+
retryable: true,
|
|
10645
|
+
message: "Agent runtime provisioning failed. Reload the workspace and try again."
|
|
10646
|
+
});
|
|
10647
|
+
readyTracker.markDegraded("runtime dependency provisioning failed");
|
|
10648
|
+
app.log.warn({ err: error, workspaceId }, "[agent] background runtime provisioning failed");
|
|
10649
|
+
throw error;
|
|
10650
|
+
}
|
|
10651
|
+
);
|
|
10652
|
+
task.catch(() => {
|
|
10653
|
+
});
|
|
10654
|
+
if (binding) binding.runtimeProvisioningTask = task;
|
|
10655
|
+
return task;
|
|
10656
|
+
};
|
|
10657
|
+
const checkReadiness = createRuntimeReadinessCheck(workspaceId, () => runtimeDependencies);
|
|
9931
10658
|
const standardTools = [
|
|
9932
10659
|
...buildHarnessAgentTools(runtimeBundle, {
|
|
9933
10660
|
getCurrent: () => runtimeProvisioning ? {
|
|
9934
10661
|
env: runtimeProvisioning.env,
|
|
9935
10662
|
pathEntries: runtimeProvisioning.pathEntries
|
|
9936
|
-
} : void 0
|
|
10663
|
+
} : void 0,
|
|
10664
|
+
getReadiness: () => checkReadiness("runtime:python", {})
|
|
9937
10665
|
}),
|
|
9938
10666
|
...buildFilesystemAgentTools(runtimeBundle),
|
|
9939
10667
|
...buildUploadAgentTools(runtimeBundle)
|
|
@@ -9966,7 +10694,8 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9966
10694
|
...scopedExtraTools
|
|
9967
10695
|
],
|
|
9968
10696
|
pluginTools,
|
|
9969
|
-
logger: app.log
|
|
10697
|
+
logger: app.log,
|
|
10698
|
+
checkReadiness
|
|
9970
10699
|
});
|
|
9971
10700
|
const harnessFactory = opts.harnessFactory ?? ((input) => createPiCodingAgentHarness({
|
|
9972
10701
|
...input,
|
|
@@ -9975,9 +10704,18 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9975
10704
|
noSkills: true,
|
|
9976
10705
|
...scope.pi,
|
|
9977
10706
|
additionalSkillPaths: [
|
|
9978
|
-
...runtimeProvisioning?.skillPaths ?? [],
|
|
9979
10707
|
...scope.pi?.additionalSkillPaths ?? []
|
|
9980
|
-
]
|
|
10708
|
+
],
|
|
10709
|
+
getHotReloadableResources: () => {
|
|
10710
|
+
const hot = scope.pi?.getHotReloadableResources?.() ?? {};
|
|
10711
|
+
return {
|
|
10712
|
+
...hot,
|
|
10713
|
+
additionalSkillPaths: [
|
|
10714
|
+
...runtimeProvisioning?.skillPaths ?? [],
|
|
10715
|
+
...hot.additionalSkillPaths ?? []
|
|
10716
|
+
]
|
|
10717
|
+
};
|
|
10718
|
+
}
|
|
9981
10719
|
}
|
|
9982
10720
|
}));
|
|
9983
10721
|
const harness = await harnessFactory({
|
|
@@ -9988,24 +10726,22 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
9988
10726
|
systemPromptDynamic: opts.getSystemPromptDynamic ? () => opts.getSystemPromptDynamic?.({ workspaceId, workspaceRoot: root }) : opts.systemPromptDynamic,
|
|
9989
10727
|
telemetry: opts.telemetry
|
|
9990
10728
|
});
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
harnessReady: true
|
|
9994
|
-
});
|
|
9995
|
-
if (resolvedMode === "vercel-sandbox") {
|
|
9996
|
-
queueMicrotask(() => readyTracker.markSandboxReady());
|
|
9997
|
-
}
|
|
9998
|
-
return {
|
|
10729
|
+
readyTracker.markHarnessReady();
|
|
10730
|
+
binding = {
|
|
9999
10731
|
runtimeBundle,
|
|
10000
10732
|
runtimeProvisioning,
|
|
10733
|
+
runtimeDependencies,
|
|
10734
|
+
runtimeProvisioningTask: void 0,
|
|
10001
10735
|
reprovision: async (reloadRequest) => {
|
|
10002
|
-
|
|
10003
|
-
return
|
|
10736
|
+
const result = await startRuntimeProvisioning(reloadRequest);
|
|
10737
|
+
return await result;
|
|
10004
10738
|
},
|
|
10005
10739
|
harness,
|
|
10006
10740
|
tools,
|
|
10007
10741
|
readyTracker
|
|
10008
10742
|
};
|
|
10743
|
+
startRuntimeProvisioning(request);
|
|
10744
|
+
return binding;
|
|
10009
10745
|
}
|
|
10010
10746
|
function createRuntimeBindingEntry(workspaceId, scope, request) {
|
|
10011
10747
|
const entry = {
|
|
@@ -10099,6 +10835,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
10099
10835
|
const hasRuntimeProvisioningInput = opts.provisionWorkspace !== false && Boolean(opts.provisionRuntime);
|
|
10100
10836
|
const staticBinding = requestScopedRuntime ? null : await getOrCreateRuntimeBinding(sessionId);
|
|
10101
10837
|
const skillsScopeByRequest = /* @__PURE__ */ new WeakMap();
|
|
10838
|
+
const earlySessionStores = /* @__PURE__ */ new Map();
|
|
10102
10839
|
function getSkillsScopeForRequest(request) {
|
|
10103
10840
|
let promise = skillsScopeByRequest.get(request);
|
|
10104
10841
|
if (!promise) {
|
|
@@ -10111,6 +10848,18 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
10111
10848
|
if (staticBinding) return staticBinding;
|
|
10112
10849
|
return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request, options);
|
|
10113
10850
|
}
|
|
10851
|
+
async function getSessionStoreForRequest(request) {
|
|
10852
|
+
if (staticBinding) return staticBinding.harness.sessions;
|
|
10853
|
+
const scope = await resolveRuntimeScope(getRequestWorkspaceId(request), request);
|
|
10854
|
+
const cached = earlySessionStores.get(scope.key);
|
|
10855
|
+
if (cached) return cached;
|
|
10856
|
+
const store = new PiSessionStore(scope.root, {
|
|
10857
|
+
sessionNamespace: scope.sessionNamespace,
|
|
10858
|
+
storageCwd: scope.root
|
|
10859
|
+
});
|
|
10860
|
+
earlySessionStores.set(scope.key, store);
|
|
10861
|
+
return store;
|
|
10862
|
+
}
|
|
10114
10863
|
const agentToolNames = staticBinding ? staticBinding.tools.map((tool) => tool.name) : [
|
|
10115
10864
|
...STANDARD_AGENT_TOOL_NAMES,
|
|
10116
10865
|
...(opts.extraTools ?? []).map((tool) => tool.name)
|
|
@@ -10179,20 +10928,18 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
10179
10928
|
});
|
|
10180
10929
|
await app.register(chatRoutes, {
|
|
10181
10930
|
getRuntime: async (request) => {
|
|
10182
|
-
const binding = await getBindingForRequest(request
|
|
10931
|
+
const binding = await getBindingForRequest(request);
|
|
10183
10932
|
return {
|
|
10184
10933
|
harness: binding.harness,
|
|
10185
10934
|
workdir: binding.runtimeBundle.workspace.root
|
|
10186
10935
|
};
|
|
10187
10936
|
},
|
|
10937
|
+
getSessionStore: getSessionStoreForRequest,
|
|
10188
10938
|
sessionChangesTracker,
|
|
10189
10939
|
telemetry: opts.telemetry
|
|
10190
10940
|
});
|
|
10191
10941
|
await app.register(sessionRoutes, {
|
|
10192
|
-
getSessionStore:
|
|
10193
|
-
const binding = await getBindingForRequest(request);
|
|
10194
|
-
return binding.harness.sessions;
|
|
10195
|
-
},
|
|
10942
|
+
getSessionStore: getSessionStoreForRequest,
|
|
10196
10943
|
getRuntime: async (request) => {
|
|
10197
10944
|
const binding = await getBindingForRequest(request);
|
|
10198
10945
|
return {
|