@odla-ai/harness 0.7.0 → 0.7.1
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/{chunk-QZXCQSPZ.js → chunk-GYWQM76X.js} +177 -28
- package/dist/chunk-GYWQM76X.js.map +1 -0
- package/dist/code-runtime-cli.cjs +176 -27
- package/dist/code-runtime-cli.cjs.map +1 -1
- package/dist/code-runtime-cli.js +1 -1
- package/dist/node.cjs +176 -27
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-QZXCQSPZ.js.map +0 -1
|
@@ -1401,7 +1401,10 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
|
|
|
1401
1401
|
var V2_SYSTEM_PROMPT = `You are the coding agent inside an odla Code harness.
|
|
1402
1402
|
Start by orienting: odla_list shows the files in the workspace and odla_search
|
|
1403
1403
|
finds a literal string across them. Prefer those over guessing a path.
|
|
1404
|
-
Then odla_read a bounded range, and odla_apply_git_diff to mutate.
|
|
1404
|
+
Then odla_read a bounded range, and odla_apply_git_diff to mutate. When you
|
|
1405
|
+
need several independent searches or file ranges, issue those read-only calls
|
|
1406
|
+
together in one turn; their results stay ordered and the harness overlaps them.
|
|
1407
|
+
Never issue odla_apply_git_diff or odla_run_recipe alongside another tool call.
|
|
1405
1408
|
For mutations, call odla_apply_git_diff with raw git diff text. It must start
|
|
1406
1409
|
with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
|
|
1407
1410
|
headers and numbered "@@" hunks, and never use "*** Begin Patch" wrappers.
|
|
@@ -1432,18 +1435,26 @@ var SYSTEM_PROMPT_FOR = {
|
|
|
1432
1435
|
};
|
|
1433
1436
|
function codeSkill(opts) {
|
|
1434
1437
|
let seq = 0;
|
|
1438
|
+
let nextCompletion = 1;
|
|
1439
|
+
const completed = /* @__PURE__ */ new Map();
|
|
1435
1440
|
const call = async (tool, input, signal) => {
|
|
1441
|
+
const sequence = ++seq;
|
|
1436
1442
|
const startedAt = Date.now();
|
|
1437
1443
|
const response2 = await opts.broker.execute(
|
|
1438
1444
|
{ lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
|
|
1439
|
-
{ requestId: `bench-${tool}-${
|
|
1445
|
+
{ requestId: `bench-${tool}-${sequence}`, tool, input }
|
|
1440
1446
|
);
|
|
1441
|
-
|
|
1447
|
+
completed.set(sequence, {
|
|
1442
1448
|
tool,
|
|
1443
1449
|
ok: response2.ok,
|
|
1444
1450
|
durationMs: Date.now() - startedAt,
|
|
1445
1451
|
...response2.ok ? {} : { error: String(response2.content).slice(0, 300) }
|
|
1446
1452
|
});
|
|
1453
|
+
while (completed.has(nextCompletion)) {
|
|
1454
|
+
const completion = completed.get(nextCompletion);
|
|
1455
|
+
completed.delete(nextCompletion++);
|
|
1456
|
+
opts.onToolCall?.(completion);
|
|
1457
|
+
}
|
|
1447
1458
|
return { content: response2.content, isError: !response2.ok };
|
|
1448
1459
|
};
|
|
1449
1460
|
const read2 = {
|
|
@@ -1459,6 +1470,7 @@ function codeSkill(opts) {
|
|
|
1459
1470
|
},
|
|
1460
1471
|
additionalProperties: false
|
|
1461
1472
|
},
|
|
1473
|
+
concurrency: "parallel",
|
|
1462
1474
|
handler: (input, ctx) => call("sandbox.read", input, ctx.signal)
|
|
1463
1475
|
};
|
|
1464
1476
|
const applyPatch = {
|
|
@@ -1500,6 +1512,7 @@ function codeSkill(opts) {
|
|
|
1500
1512
|
},
|
|
1501
1513
|
additionalProperties: false
|
|
1502
1514
|
},
|
|
1515
|
+
concurrency: "parallel",
|
|
1503
1516
|
handler: (input, ctx) => call("sandbox.list", input, ctx.signal)
|
|
1504
1517
|
};
|
|
1505
1518
|
const searchFiles = {
|
|
@@ -1516,11 +1529,13 @@ function codeSkill(opts) {
|
|
|
1516
1529
|
},
|
|
1517
1530
|
additionalProperties: false
|
|
1518
1531
|
},
|
|
1532
|
+
concurrency: "parallel",
|
|
1519
1533
|
handler: (input, ctx) => call("sandbox.search", input, ctx.signal)
|
|
1520
1534
|
};
|
|
1521
1535
|
const graphTool = (name, tool, description, required) => ({
|
|
1522
1536
|
name,
|
|
1523
1537
|
description,
|
|
1538
|
+
concurrency: "parallel",
|
|
1524
1539
|
inputSchema: {
|
|
1525
1540
|
type: "object",
|
|
1526
1541
|
...required ? { required: ["query"] } : {},
|
|
@@ -1950,11 +1965,30 @@ function response(request, ok, content, details) {
|
|
|
1950
1965
|
var import_promises9 = require("fs/promises");
|
|
1951
1966
|
|
|
1952
1967
|
// src/code-tool-discovery.ts
|
|
1968
|
+
var import_node_child_process5 = require("child_process");
|
|
1953
1969
|
var import_promises7 = require("fs/promises");
|
|
1954
1970
|
var import_node_path8 = require("path");
|
|
1955
1971
|
var DEFAULT_MAX_FILES = 2e4;
|
|
1956
1972
|
var DEFAULT_MAX_RESULTS = 100;
|
|
1957
1973
|
var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
1974
|
+
function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = registeredFiles) {
|
|
1975
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
1976
|
+
return {
|
|
1977
|
+
files(root) {
|
|
1978
|
+
const existing = cache2.get(root);
|
|
1979
|
+
if (existing) return existing;
|
|
1980
|
+
const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
|
|
1981
|
+
cache2.set(root, pending);
|
|
1982
|
+
void pending.catch(() => {
|
|
1983
|
+
if (cache2.get(root) === pending) cache2.delete(root);
|
|
1984
|
+
});
|
|
1985
|
+
return pending;
|
|
1986
|
+
},
|
|
1987
|
+
invalidate(root) {
|
|
1988
|
+
cache2.delete(root);
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1958
1992
|
async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
1959
1993
|
const paths = [];
|
|
1960
1994
|
const walk = async (directory) => {
|
|
@@ -1985,28 +2019,122 @@ function listWorkspace(paths, options = {}) {
|
|
|
1985
2019
|
return scoped.slice(0, max);
|
|
1986
2020
|
}
|
|
1987
2021
|
async function searchWorkspace(root, paths, options) {
|
|
1988
|
-
|
|
1989
|
-
if (!query) throw new TypeError("search query must be a non-empty string");
|
|
2022
|
+
options.signal?.throwIfAborted();
|
|
2023
|
+
if (!options.query) throw new TypeError("search query must be a non-empty string");
|
|
1990
2024
|
const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
1991
2025
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
1992
2026
|
const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
|
|
2027
|
+
if (scoped.length === 0) return [];
|
|
2028
|
+
try {
|
|
2029
|
+
return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
options.signal?.throwIfAborted();
|
|
2032
|
+
return fallbackSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
var MAX_NATIVE_ARG_BYTES = 96 * 1024;
|
|
2036
|
+
async function nativeSearch(root, paths, options) {
|
|
2037
|
+
const batches = [];
|
|
2038
|
+
let batch = [];
|
|
2039
|
+
let bytes = 0;
|
|
2040
|
+
for (const path of paths) {
|
|
2041
|
+
const size = Buffer.byteLength(path) + 1;
|
|
2042
|
+
if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
|
|
2043
|
+
batches.push(batch);
|
|
2044
|
+
batch = [];
|
|
2045
|
+
bytes = 0;
|
|
2046
|
+
}
|
|
2047
|
+
batch.push(path);
|
|
2048
|
+
bytes += size;
|
|
2049
|
+
}
|
|
2050
|
+
if (batch.length > 0) batches.push(batch);
|
|
2051
|
+
const matches = [];
|
|
2052
|
+
for (const files of batches) {
|
|
2053
|
+
const remaining = options.maxResults - matches.length;
|
|
2054
|
+
if (remaining <= 0) break;
|
|
2055
|
+
matches.push(...await nativeSearchBatch(root, files, options, remaining));
|
|
2056
|
+
}
|
|
2057
|
+
return matches;
|
|
2058
|
+
}
|
|
2059
|
+
function nativeSearchBatch(root, paths, options, remaining) {
|
|
2060
|
+
return new Promise((resolveMatches, reject) => {
|
|
2061
|
+
const args = [
|
|
2062
|
+
"--fixed-strings",
|
|
2063
|
+
"--json",
|
|
2064
|
+
"--no-messages",
|
|
2065
|
+
"--sort=path",
|
|
2066
|
+
`--max-filesize=${options.maxFileBytes}`,
|
|
2067
|
+
"--max-columns=4096",
|
|
2068
|
+
"--max-columns-preview",
|
|
2069
|
+
options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
|
|
2070
|
+
"--",
|
|
2071
|
+
options.query,
|
|
2072
|
+
...paths
|
|
2073
|
+
];
|
|
2074
|
+
const child = (0, import_node_child_process5.spawn)("rg", args, {
|
|
2075
|
+
cwd: root,
|
|
2076
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2077
|
+
...options.signal ? { signal: options.signal } : {}
|
|
2078
|
+
});
|
|
2079
|
+
const matches = [];
|
|
2080
|
+
let carry = "";
|
|
2081
|
+
let stopped = false;
|
|
2082
|
+
const consume = (line) => {
|
|
2083
|
+
if (matches.length >= remaining) return;
|
|
2084
|
+
let event;
|
|
2085
|
+
try {
|
|
2086
|
+
event = JSON.parse(line);
|
|
2087
|
+
} catch {
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
const path = event.data?.path?.text;
|
|
2091
|
+
const lineNumber = event.data?.line_number;
|
|
2092
|
+
const source = event.data?.lines?.text;
|
|
2093
|
+
if (event.type !== "match" || path === void 0 || lineNumber === void 0 || source === void 0) return;
|
|
2094
|
+
matches.push({ path, line: lineNumber, text: source.trim().slice(0, 240) });
|
|
2095
|
+
if (matches.length >= remaining) {
|
|
2096
|
+
stopped = true;
|
|
2097
|
+
child.kill();
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
child.stdout.setEncoding("utf8");
|
|
2101
|
+
child.stdout.on("data", (chunk) => {
|
|
2102
|
+
carry += chunk;
|
|
2103
|
+
let newline = carry.indexOf("\n");
|
|
2104
|
+
while (newline >= 0) {
|
|
2105
|
+
consume(carry.slice(0, newline));
|
|
2106
|
+
carry = carry.slice(newline + 1);
|
|
2107
|
+
newline = carry.indexOf("\n");
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
child.once("error", reject);
|
|
2111
|
+
child.once("close", (code) => {
|
|
2112
|
+
if (carry) consume(carry);
|
|
2113
|
+
if (stopped || code === 0 || code === 1) resolveMatches(matches);
|
|
2114
|
+
else reject(new Error(`native search exited with status ${code ?? "unknown"}`));
|
|
2115
|
+
});
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
async function fallbackSearch(root, scoped, options) {
|
|
2119
|
+
const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
|
|
1993
2120
|
const matches = [];
|
|
1994
2121
|
for (const path of scoped) {
|
|
1995
|
-
|
|
2122
|
+
options.signal?.throwIfAborted();
|
|
2123
|
+
if (matches.length >= options.maxResults) break;
|
|
1996
2124
|
let source;
|
|
1997
2125
|
try {
|
|
1998
2126
|
source = await (0, import_promises7.readFile)((0, import_node_path8.resolve)(root, path));
|
|
1999
2127
|
} catch {
|
|
2000
2128
|
continue;
|
|
2001
2129
|
}
|
|
2002
|
-
if (source.byteLength > maxFileBytes || source.includes(0)) continue;
|
|
2130
|
+
if (source.byteLength > options.maxFileBytes || source.includes(0)) continue;
|
|
2003
2131
|
const lines = source.toString("utf8").split("\n");
|
|
2004
2132
|
for (let index = 0; index < lines.length; index += 1) {
|
|
2005
2133
|
const raw = lines[index];
|
|
2006
2134
|
const haystack = options.caseSensitive === false ? raw.toLowerCase() : raw;
|
|
2007
2135
|
if (!haystack.includes(query)) continue;
|
|
2008
2136
|
matches.push({ path, line: index + 1, text: raw.trim().slice(0, 240) });
|
|
2009
|
-
if (matches.length >= maxResults) break;
|
|
2137
|
+
if (matches.length >= options.maxResults) break;
|
|
2010
2138
|
}
|
|
2011
2139
|
}
|
|
2012
2140
|
return matches;
|
|
@@ -2031,6 +2159,9 @@ function workspaceGraphs(workspaceDir, paths) {
|
|
|
2031
2159
|
cache.set(workspaceDir, built);
|
|
2032
2160
|
return built;
|
|
2033
2161
|
}
|
|
2162
|
+
function forgetWorkspaceGraphs(workspaceDir) {
|
|
2163
|
+
cache.delete(workspaceDir);
|
|
2164
|
+
}
|
|
2034
2165
|
var shortId = (id) => id.slice(id.indexOf(":") + 1);
|
|
2035
2166
|
function renderOverview(graphs, prefix) {
|
|
2036
2167
|
const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
|
|
@@ -2077,7 +2208,7 @@ var GRAPH_TOOLS = /* @__PURE__ */ new Set([
|
|
|
2077
2208
|
"sandbox.who_imports",
|
|
2078
2209
|
"sandbox.who_touches"
|
|
2079
2210
|
]);
|
|
2080
|
-
async function read(context, request, options, policy) {
|
|
2211
|
+
async function read(context, request, options, policy, registry) {
|
|
2081
2212
|
exactKeys(request.input, ["path", "startLine", "endLine"]);
|
|
2082
2213
|
const path = stringField(request.input, "path");
|
|
2083
2214
|
const startLine = optionalInteger(request.input.startLine) ?? 1;
|
|
@@ -2085,7 +2216,7 @@ async function read(context, request, options, policy) {
|
|
|
2085
2216
|
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
2086
2217
|
throw new TypeError("requested line range exceeds its bound");
|
|
2087
2218
|
}
|
|
2088
|
-
const paths = await
|
|
2219
|
+
const paths = await registry.files(context.workspaceDir);
|
|
2089
2220
|
if (!paths.includes(path)) {
|
|
2090
2221
|
throw new TypeError(`no such file in the staged workspace: "${path}". Use sandbox.overview, sandbox.where_is or sandbox.search to find the correct path.`);
|
|
2091
2222
|
}
|
|
@@ -2105,13 +2236,13 @@ async function read(context, request, options, policy) {
|
|
|
2105
2236
|
}
|
|
2106
2237
|
return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
2107
2238
|
}
|
|
2108
|
-
async function list(context, request, options, policy) {
|
|
2239
|
+
async function list(context, request, options, policy, registry) {
|
|
2109
2240
|
exactKeys(request.input, ["prefix", "maxEntries"]);
|
|
2110
2241
|
const raw = request.input.prefix;
|
|
2111
2242
|
const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
2112
2243
|
const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
|
|
2113
2244
|
if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
|
|
2114
|
-
const paths = await
|
|
2245
|
+
const paths = await registry.files(context.workspaceDir);
|
|
2115
2246
|
const allowed = await policy.list(policyContext(context, request, options, { paths, ...prefix ? { prefix } : {} }));
|
|
2116
2247
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
2117
2248
|
const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
|
|
@@ -2129,7 +2260,7 @@ async function list(context, request, options, policy) {
|
|
|
2129
2260
|
{ count: entries.length, truncated }
|
|
2130
2261
|
);
|
|
2131
2262
|
}
|
|
2132
|
-
async function search(context, request, options, policy) {
|
|
2263
|
+
async function search(context, request, options, policy, registry) {
|
|
2133
2264
|
exactKeys(request.input, ["query", "prefix", "maxResults", "caseSensitive"]);
|
|
2134
2265
|
const query = stringField(request.input, "query");
|
|
2135
2266
|
if (query.length > 512) throw new TypeError("search query exceeds its bound");
|
|
@@ -2138,21 +2269,22 @@ async function search(context, request, options, policy) {
|
|
|
2138
2269
|
const maxResults = optionalInteger(request.input.maxResults) ?? 100;
|
|
2139
2270
|
if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
|
|
2140
2271
|
const caseSensitive = request.input.caseSensitive === void 0 ? true : request.input.caseSensitive === true;
|
|
2141
|
-
const paths = await
|
|
2272
|
+
const paths = await registry.files(context.workspaceDir);
|
|
2142
2273
|
const allowed = await policy.search(policyContext(context, request, options, { paths, query, ...prefix ? { prefix } : {} }));
|
|
2143
2274
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
2144
2275
|
const matches = await searchWorkspace(context.workspaceDir, paths, {
|
|
2145
2276
|
query,
|
|
2146
2277
|
maxResults,
|
|
2147
2278
|
caseSensitive,
|
|
2148
|
-
...prefix ? { prefix } : {}
|
|
2279
|
+
...prefix ? { prefix } : {},
|
|
2280
|
+
...context.signal ? { signal: context.signal } : {}
|
|
2149
2281
|
});
|
|
2150
2282
|
if (!matches.length) return response(request, true, `No match for "${query}".`, { count: 0 });
|
|
2151
2283
|
return response(request, true, matches.map((match) => `${match.path}:${match.line}: ${match.text}`).join("\n"), {
|
|
2152
2284
|
count: matches.length
|
|
2153
2285
|
});
|
|
2154
2286
|
}
|
|
2155
|
-
async function graphQuery(context, request, options, policy) {
|
|
2287
|
+
async function graphQuery(context, request, options, policy, registry) {
|
|
2156
2288
|
exactKeys(request.input, ["query"]);
|
|
2157
2289
|
const raw = request.input.query;
|
|
2158
2290
|
const query = typeof raw === "string" ? raw : "";
|
|
@@ -2162,7 +2294,7 @@ async function graphQuery(context, request, options, policy) {
|
|
|
2162
2294
|
selector: query
|
|
2163
2295
|
}));
|
|
2164
2296
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
2165
|
-
const paths = await
|
|
2297
|
+
const paths = await registry.files(context.workspaceDir);
|
|
2166
2298
|
const graphs = await workspaceGraphs(context.workspaceDir, paths);
|
|
2167
2299
|
if (request.tool === "sandbox.overview") {
|
|
2168
2300
|
return response(request, true, renderOverview(graphs, query || void 0));
|
|
@@ -2178,23 +2310,38 @@ function createCodeToolBroker(options) {
|
|
|
2178
2310
|
validateOptions(options);
|
|
2179
2311
|
const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
|
|
2180
2312
|
const policy = createCodePolicyGate(options);
|
|
2181
|
-
|
|
2313
|
+
const registry = createWorkspaceFileRegistry();
|
|
2314
|
+
let barrier = Promise.resolve();
|
|
2315
|
+
const activeReads = /* @__PURE__ */ new Set();
|
|
2182
2316
|
return {
|
|
2183
2317
|
execute(context, request) {
|
|
2184
|
-
|
|
2185
|
-
|
|
2318
|
+
if (isReadTool(request.tool)) {
|
|
2319
|
+
const result2 = barrier.then(() => route(context, request, options, recipes, policy, registry));
|
|
2320
|
+
const settled = result2.then(() => void 0, () => void 0);
|
|
2321
|
+
activeReads.add(settled);
|
|
2322
|
+
void settled.then(() => {
|
|
2323
|
+
activeReads.delete(settled);
|
|
2324
|
+
});
|
|
2325
|
+
return result2;
|
|
2326
|
+
}
|
|
2327
|
+
const earlierReads = [...activeReads];
|
|
2328
|
+
const result = barrier.then(() => Promise.all(earlierReads)).then(() => route(context, request, options, recipes, policy, registry));
|
|
2329
|
+
barrier = result.then(() => void 0, () => void 0);
|
|
2186
2330
|
return result;
|
|
2187
2331
|
}
|
|
2188
2332
|
};
|
|
2189
2333
|
}
|
|
2190
|
-
|
|
2334
|
+
function isReadTool(tool) {
|
|
2335
|
+
return tool === "sandbox.read" || tool === "sandbox.list" || tool === "sandbox.search" || GRAPH_TOOLS.has(tool);
|
|
2336
|
+
}
|
|
2337
|
+
async function route(context, request, options, recipes, policy, registry) {
|
|
2191
2338
|
try {
|
|
2192
2339
|
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
2193
|
-
if (request.tool === "sandbox.read") return await read(context, request, options, policy);
|
|
2194
|
-
if (request.tool === "sandbox.list") return await list(context, request, options, policy);
|
|
2195
|
-
if (request.tool === "sandbox.search") return await search(context, request, options, policy);
|
|
2196
|
-
if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy);
|
|
2197
|
-
if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
|
|
2340
|
+
if (request.tool === "sandbox.read") return await read(context, request, options, policy, registry);
|
|
2341
|
+
if (request.tool === "sandbox.list") return await list(context, request, options, policy, registry);
|
|
2342
|
+
if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
|
|
2343
|
+
if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
|
|
2344
|
+
if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
|
|
2198
2345
|
return await recipe(context, request, options, recipes, policy);
|
|
2199
2346
|
} catch (reason) {
|
|
2200
2347
|
return response(request, false, toolFailureMessage(reason));
|
|
@@ -2209,7 +2356,7 @@ function toolFailureMessage(reason) {
|
|
|
2209
2356
|
if (code === "EACCES" || code === "EPERM") return "that path is not readable through this tool";
|
|
2210
2357
|
return "tool failed closed";
|
|
2211
2358
|
}
|
|
2212
|
-
async function patch(context, request, options, policy) {
|
|
2359
|
+
async function patch(context, request, options, policy, registry) {
|
|
2213
2360
|
exactKeys(request.input, ["patch"]);
|
|
2214
2361
|
const value = stringField(request.input, "patch");
|
|
2215
2362
|
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
@@ -2219,6 +2366,8 @@ async function patch(context, request, options, policy) {
|
|
|
2219
2366
|
const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
|
|
2220
2367
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
2221
2368
|
await applyCodePatch(context.workspaceDir, value, paths);
|
|
2369
|
+
registry.invalidate(context.workspaceDir);
|
|
2370
|
+
forgetWorkspaceGraphs(context.workspaceDir);
|
|
2222
2371
|
return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
2223
2372
|
}
|
|
2224
2373
|
async function recipe(context, request, options, recipes, policy) {
|