@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
package/dist/code-runtime-cli.js
CHANGED
package/dist/node.cjs
CHANGED
|
@@ -1983,7 +1983,10 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
|
|
|
1983
1983
|
var V2_SYSTEM_PROMPT = `You are the coding agent inside an odla Code harness.
|
|
1984
1984
|
Start by orienting: odla_list shows the files in the workspace and odla_search
|
|
1985
1985
|
finds a literal string across them. Prefer those over guessing a path.
|
|
1986
|
-
Then odla_read a bounded range, and odla_apply_git_diff to mutate.
|
|
1986
|
+
Then odla_read a bounded range, and odla_apply_git_diff to mutate. When you
|
|
1987
|
+
need several independent searches or file ranges, issue those read-only calls
|
|
1988
|
+
together in one turn; their results stay ordered and the harness overlaps them.
|
|
1989
|
+
Never issue odla_apply_git_diff or odla_run_recipe alongside another tool call.
|
|
1987
1990
|
For mutations, call odla_apply_git_diff with raw git diff text. It must start
|
|
1988
1991
|
with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
|
|
1989
1992
|
headers and numbered "@@" hunks, and never use "*** Begin Patch" wrappers.
|
|
@@ -2014,18 +2017,26 @@ var SYSTEM_PROMPT_FOR = {
|
|
|
2014
2017
|
};
|
|
2015
2018
|
function codeSkill(opts) {
|
|
2016
2019
|
let seq = 0;
|
|
2020
|
+
let nextCompletion = 1;
|
|
2021
|
+
const completed = /* @__PURE__ */ new Map();
|
|
2017
2022
|
const call = async (tool, input, signal) => {
|
|
2023
|
+
const sequence = ++seq;
|
|
2018
2024
|
const startedAt = Date.now();
|
|
2019
2025
|
const response2 = await opts.broker.execute(
|
|
2020
2026
|
{ lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
|
|
2021
|
-
{ requestId: `bench-${tool}-${
|
|
2027
|
+
{ requestId: `bench-${tool}-${sequence}`, tool, input }
|
|
2022
2028
|
);
|
|
2023
|
-
|
|
2029
|
+
completed.set(sequence, {
|
|
2024
2030
|
tool,
|
|
2025
2031
|
ok: response2.ok,
|
|
2026
2032
|
durationMs: Date.now() - startedAt,
|
|
2027
2033
|
...response2.ok ? {} : { error: String(response2.content).slice(0, 300) }
|
|
2028
2034
|
});
|
|
2035
|
+
while (completed.has(nextCompletion)) {
|
|
2036
|
+
const completion = completed.get(nextCompletion);
|
|
2037
|
+
completed.delete(nextCompletion++);
|
|
2038
|
+
opts.onToolCall?.(completion);
|
|
2039
|
+
}
|
|
2029
2040
|
return { content: response2.content, isError: !response2.ok };
|
|
2030
2041
|
};
|
|
2031
2042
|
const read2 = {
|
|
@@ -2041,6 +2052,7 @@ function codeSkill(opts) {
|
|
|
2041
2052
|
},
|
|
2042
2053
|
additionalProperties: false
|
|
2043
2054
|
},
|
|
2055
|
+
concurrency: "parallel",
|
|
2044
2056
|
handler: (input, ctx) => call("sandbox.read", input, ctx.signal)
|
|
2045
2057
|
};
|
|
2046
2058
|
const applyPatch = {
|
|
@@ -2082,6 +2094,7 @@ function codeSkill(opts) {
|
|
|
2082
2094
|
},
|
|
2083
2095
|
additionalProperties: false
|
|
2084
2096
|
},
|
|
2097
|
+
concurrency: "parallel",
|
|
2085
2098
|
handler: (input, ctx) => call("sandbox.list", input, ctx.signal)
|
|
2086
2099
|
};
|
|
2087
2100
|
const searchFiles = {
|
|
@@ -2098,11 +2111,13 @@ function codeSkill(opts) {
|
|
|
2098
2111
|
},
|
|
2099
2112
|
additionalProperties: false
|
|
2100
2113
|
},
|
|
2114
|
+
concurrency: "parallel",
|
|
2101
2115
|
handler: (input, ctx) => call("sandbox.search", input, ctx.signal)
|
|
2102
2116
|
};
|
|
2103
2117
|
const graphTool = (name, tool, description, required) => ({
|
|
2104
2118
|
name,
|
|
2105
2119
|
description,
|
|
2120
|
+
concurrency: "parallel",
|
|
2106
2121
|
inputSchema: {
|
|
2107
2122
|
type: "object",
|
|
2108
2123
|
...required ? { required: ["query"] } : {},
|
|
@@ -2532,11 +2547,30 @@ function response(request, ok, content, details) {
|
|
|
2532
2547
|
var import_promises11 = require("fs/promises");
|
|
2533
2548
|
|
|
2534
2549
|
// src/code-tool-discovery.ts
|
|
2550
|
+
var import_node_child_process6 = require("child_process");
|
|
2535
2551
|
var import_promises9 = require("fs/promises");
|
|
2536
2552
|
var import_node_path9 = require("path");
|
|
2537
2553
|
var DEFAULT_MAX_FILES = 2e4;
|
|
2538
2554
|
var DEFAULT_MAX_RESULTS = 100;
|
|
2539
2555
|
var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
2556
|
+
function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = registeredFiles) {
|
|
2557
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
2558
|
+
return {
|
|
2559
|
+
files(root) {
|
|
2560
|
+
const existing = cache2.get(root);
|
|
2561
|
+
if (existing) return existing;
|
|
2562
|
+
const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
|
|
2563
|
+
cache2.set(root, pending);
|
|
2564
|
+
void pending.catch(() => {
|
|
2565
|
+
if (cache2.get(root) === pending) cache2.delete(root);
|
|
2566
|
+
});
|
|
2567
|
+
return pending;
|
|
2568
|
+
},
|
|
2569
|
+
invalidate(root) {
|
|
2570
|
+
cache2.delete(root);
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2540
2574
|
async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
|
|
2541
2575
|
const paths = [];
|
|
2542
2576
|
const walk = async (directory) => {
|
|
@@ -2567,28 +2601,122 @@ function listWorkspace(paths, options = {}) {
|
|
|
2567
2601
|
return scoped.slice(0, max);
|
|
2568
2602
|
}
|
|
2569
2603
|
async function searchWorkspace(root, paths, options) {
|
|
2570
|
-
|
|
2571
|
-
if (!query) throw new TypeError("search query must be a non-empty string");
|
|
2604
|
+
options.signal?.throwIfAborted();
|
|
2605
|
+
if (!options.query) throw new TypeError("search query must be a non-empty string");
|
|
2572
2606
|
const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
2573
2607
|
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
2574
2608
|
const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
|
|
2609
|
+
if (scoped.length === 0) return [];
|
|
2610
|
+
try {
|
|
2611
|
+
return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
options.signal?.throwIfAborted();
|
|
2614
|
+
return fallbackSearch(root, scoped, { ...options, maxResults, maxFileBytes });
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
var MAX_NATIVE_ARG_BYTES = 96 * 1024;
|
|
2618
|
+
async function nativeSearch(root, paths, options) {
|
|
2619
|
+
const batches = [];
|
|
2620
|
+
let batch = [];
|
|
2621
|
+
let bytes = 0;
|
|
2622
|
+
for (const path of paths) {
|
|
2623
|
+
const size = Buffer.byteLength(path) + 1;
|
|
2624
|
+
if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
|
|
2625
|
+
batches.push(batch);
|
|
2626
|
+
batch = [];
|
|
2627
|
+
bytes = 0;
|
|
2628
|
+
}
|
|
2629
|
+
batch.push(path);
|
|
2630
|
+
bytes += size;
|
|
2631
|
+
}
|
|
2632
|
+
if (batch.length > 0) batches.push(batch);
|
|
2633
|
+
const matches = [];
|
|
2634
|
+
for (const files of batches) {
|
|
2635
|
+
const remaining = options.maxResults - matches.length;
|
|
2636
|
+
if (remaining <= 0) break;
|
|
2637
|
+
matches.push(...await nativeSearchBatch(root, files, options, remaining));
|
|
2638
|
+
}
|
|
2639
|
+
return matches;
|
|
2640
|
+
}
|
|
2641
|
+
function nativeSearchBatch(root, paths, options, remaining) {
|
|
2642
|
+
return new Promise((resolveMatches, reject) => {
|
|
2643
|
+
const args = [
|
|
2644
|
+
"--fixed-strings",
|
|
2645
|
+
"--json",
|
|
2646
|
+
"--no-messages",
|
|
2647
|
+
"--sort=path",
|
|
2648
|
+
`--max-filesize=${options.maxFileBytes}`,
|
|
2649
|
+
"--max-columns=4096",
|
|
2650
|
+
"--max-columns-preview",
|
|
2651
|
+
options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
|
|
2652
|
+
"--",
|
|
2653
|
+
options.query,
|
|
2654
|
+
...paths
|
|
2655
|
+
];
|
|
2656
|
+
const child = (0, import_node_child_process6.spawn)("rg", args, {
|
|
2657
|
+
cwd: root,
|
|
2658
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2659
|
+
...options.signal ? { signal: options.signal } : {}
|
|
2660
|
+
});
|
|
2661
|
+
const matches = [];
|
|
2662
|
+
let carry = "";
|
|
2663
|
+
let stopped = false;
|
|
2664
|
+
const consume = (line) => {
|
|
2665
|
+
if (matches.length >= remaining) return;
|
|
2666
|
+
let event;
|
|
2667
|
+
try {
|
|
2668
|
+
event = JSON.parse(line);
|
|
2669
|
+
} catch {
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const path = event.data?.path?.text;
|
|
2673
|
+
const lineNumber = event.data?.line_number;
|
|
2674
|
+
const source = event.data?.lines?.text;
|
|
2675
|
+
if (event.type !== "match" || path === void 0 || lineNumber === void 0 || source === void 0) return;
|
|
2676
|
+
matches.push({ path, line: lineNumber, text: source.trim().slice(0, 240) });
|
|
2677
|
+
if (matches.length >= remaining) {
|
|
2678
|
+
stopped = true;
|
|
2679
|
+
child.kill();
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
child.stdout.setEncoding("utf8");
|
|
2683
|
+
child.stdout.on("data", (chunk) => {
|
|
2684
|
+
carry += chunk;
|
|
2685
|
+
let newline = carry.indexOf("\n");
|
|
2686
|
+
while (newline >= 0) {
|
|
2687
|
+
consume(carry.slice(0, newline));
|
|
2688
|
+
carry = carry.slice(newline + 1);
|
|
2689
|
+
newline = carry.indexOf("\n");
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
child.once("error", reject);
|
|
2693
|
+
child.once("close", (code) => {
|
|
2694
|
+
if (carry) consume(carry);
|
|
2695
|
+
if (stopped || code === 0 || code === 1) resolveMatches(matches);
|
|
2696
|
+
else reject(new Error(`native search exited with status ${code ?? "unknown"}`));
|
|
2697
|
+
});
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
async function fallbackSearch(root, scoped, options) {
|
|
2701
|
+
const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
|
|
2575
2702
|
const matches = [];
|
|
2576
2703
|
for (const path of scoped) {
|
|
2577
|
-
|
|
2704
|
+
options.signal?.throwIfAborted();
|
|
2705
|
+
if (matches.length >= options.maxResults) break;
|
|
2578
2706
|
let source;
|
|
2579
2707
|
try {
|
|
2580
2708
|
source = await (0, import_promises9.readFile)((0, import_node_path9.resolve)(root, path));
|
|
2581
2709
|
} catch {
|
|
2582
2710
|
continue;
|
|
2583
2711
|
}
|
|
2584
|
-
if (source.byteLength > maxFileBytes || source.includes(0)) continue;
|
|
2712
|
+
if (source.byteLength > options.maxFileBytes || source.includes(0)) continue;
|
|
2585
2713
|
const lines = source.toString("utf8").split("\n");
|
|
2586
2714
|
for (let index = 0; index < lines.length; index += 1) {
|
|
2587
2715
|
const raw = lines[index];
|
|
2588
2716
|
const haystack = options.caseSensitive === false ? raw.toLowerCase() : raw;
|
|
2589
2717
|
if (!haystack.includes(query)) continue;
|
|
2590
2718
|
matches.push({ path, line: index + 1, text: raw.trim().slice(0, 240) });
|
|
2591
|
-
if (matches.length >= maxResults) break;
|
|
2719
|
+
if (matches.length >= options.maxResults) break;
|
|
2592
2720
|
}
|
|
2593
2721
|
}
|
|
2594
2722
|
return matches;
|
|
@@ -2613,6 +2741,9 @@ function workspaceGraphs(workspaceDir, paths) {
|
|
|
2613
2741
|
cache.set(workspaceDir, built);
|
|
2614
2742
|
return built;
|
|
2615
2743
|
}
|
|
2744
|
+
function forgetWorkspaceGraphs(workspaceDir) {
|
|
2745
|
+
cache.delete(workspaceDir);
|
|
2746
|
+
}
|
|
2616
2747
|
var shortId = (id) => id.slice(id.indexOf(":") + 1);
|
|
2617
2748
|
function renderOverview(graphs, prefix) {
|
|
2618
2749
|
const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
|
|
@@ -2659,7 +2790,7 @@ var GRAPH_TOOLS = /* @__PURE__ */ new Set([
|
|
|
2659
2790
|
"sandbox.who_imports",
|
|
2660
2791
|
"sandbox.who_touches"
|
|
2661
2792
|
]);
|
|
2662
|
-
async function read(context, request, options, policy) {
|
|
2793
|
+
async function read(context, request, options, policy, registry) {
|
|
2663
2794
|
exactKeys(request.input, ["path", "startLine", "endLine"]);
|
|
2664
2795
|
const path = stringField(request.input, "path");
|
|
2665
2796
|
const startLine = optionalInteger(request.input.startLine) ?? 1;
|
|
@@ -2667,7 +2798,7 @@ async function read(context, request, options, policy) {
|
|
|
2667
2798
|
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
2668
2799
|
throw new TypeError("requested line range exceeds its bound");
|
|
2669
2800
|
}
|
|
2670
|
-
const paths = await
|
|
2801
|
+
const paths = await registry.files(context.workspaceDir);
|
|
2671
2802
|
if (!paths.includes(path)) {
|
|
2672
2803
|
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.`);
|
|
2673
2804
|
}
|
|
@@ -2687,13 +2818,13 @@ async function read(context, request, options, policy) {
|
|
|
2687
2818
|
}
|
|
2688
2819
|
return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
2689
2820
|
}
|
|
2690
|
-
async function list(context, request, options, policy) {
|
|
2821
|
+
async function list(context, request, options, policy, registry) {
|
|
2691
2822
|
exactKeys(request.input, ["prefix", "maxEntries"]);
|
|
2692
2823
|
const raw = request.input.prefix;
|
|
2693
2824
|
const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
2694
2825
|
const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
|
|
2695
2826
|
if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
|
|
2696
|
-
const paths = await
|
|
2827
|
+
const paths = await registry.files(context.workspaceDir);
|
|
2697
2828
|
const allowed = await policy.list(policyContext(context, request, options, { paths, ...prefix ? { prefix } : {} }));
|
|
2698
2829
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
2699
2830
|
const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
|
|
@@ -2711,7 +2842,7 @@ async function list(context, request, options, policy) {
|
|
|
2711
2842
|
{ count: entries.length, truncated }
|
|
2712
2843
|
);
|
|
2713
2844
|
}
|
|
2714
|
-
async function search(context, request, options, policy) {
|
|
2845
|
+
async function search(context, request, options, policy, registry) {
|
|
2715
2846
|
exactKeys(request.input, ["query", "prefix", "maxResults", "caseSensitive"]);
|
|
2716
2847
|
const query = stringField(request.input, "query");
|
|
2717
2848
|
if (query.length > 512) throw new TypeError("search query exceeds its bound");
|
|
@@ -2720,21 +2851,22 @@ async function search(context, request, options, policy) {
|
|
|
2720
2851
|
const maxResults = optionalInteger(request.input.maxResults) ?? 100;
|
|
2721
2852
|
if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
|
|
2722
2853
|
const caseSensitive = request.input.caseSensitive === void 0 ? true : request.input.caseSensitive === true;
|
|
2723
|
-
const paths = await
|
|
2854
|
+
const paths = await registry.files(context.workspaceDir);
|
|
2724
2855
|
const allowed = await policy.search(policyContext(context, request, options, { paths, query, ...prefix ? { prefix } : {} }));
|
|
2725
2856
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
2726
2857
|
const matches = await searchWorkspace(context.workspaceDir, paths, {
|
|
2727
2858
|
query,
|
|
2728
2859
|
maxResults,
|
|
2729
2860
|
caseSensitive,
|
|
2730
|
-
...prefix ? { prefix } : {}
|
|
2861
|
+
...prefix ? { prefix } : {},
|
|
2862
|
+
...context.signal ? { signal: context.signal } : {}
|
|
2731
2863
|
});
|
|
2732
2864
|
if (!matches.length) return response(request, true, `No match for "${query}".`, { count: 0 });
|
|
2733
2865
|
return response(request, true, matches.map((match) => `${match.path}:${match.line}: ${match.text}`).join("\n"), {
|
|
2734
2866
|
count: matches.length
|
|
2735
2867
|
});
|
|
2736
2868
|
}
|
|
2737
|
-
async function graphQuery(context, request, options, policy) {
|
|
2869
|
+
async function graphQuery(context, request, options, policy, registry) {
|
|
2738
2870
|
exactKeys(request.input, ["query"]);
|
|
2739
2871
|
const raw = request.input.query;
|
|
2740
2872
|
const query = typeof raw === "string" ? raw : "";
|
|
@@ -2744,7 +2876,7 @@ async function graphQuery(context, request, options, policy) {
|
|
|
2744
2876
|
selector: query
|
|
2745
2877
|
}));
|
|
2746
2878
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
2747
|
-
const paths = await
|
|
2879
|
+
const paths = await registry.files(context.workspaceDir);
|
|
2748
2880
|
const graphs = await workspaceGraphs(context.workspaceDir, paths);
|
|
2749
2881
|
if (request.tool === "sandbox.overview") {
|
|
2750
2882
|
return response(request, true, renderOverview(graphs, query || void 0));
|
|
@@ -2760,23 +2892,38 @@ function createCodeToolBroker(options) {
|
|
|
2760
2892
|
validateOptions(options);
|
|
2761
2893
|
const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
|
|
2762
2894
|
const policy = createCodePolicyGate(options);
|
|
2763
|
-
|
|
2895
|
+
const registry = createWorkspaceFileRegistry();
|
|
2896
|
+
let barrier = Promise.resolve();
|
|
2897
|
+
const activeReads = /* @__PURE__ */ new Set();
|
|
2764
2898
|
return {
|
|
2765
2899
|
execute(context, request) {
|
|
2766
|
-
|
|
2767
|
-
|
|
2900
|
+
if (isReadTool(request.tool)) {
|
|
2901
|
+
const result2 = barrier.then(() => route(context, request, options, recipes, policy, registry));
|
|
2902
|
+
const settled = result2.then(() => void 0, () => void 0);
|
|
2903
|
+
activeReads.add(settled);
|
|
2904
|
+
void settled.then(() => {
|
|
2905
|
+
activeReads.delete(settled);
|
|
2906
|
+
});
|
|
2907
|
+
return result2;
|
|
2908
|
+
}
|
|
2909
|
+
const earlierReads = [...activeReads];
|
|
2910
|
+
const result = barrier.then(() => Promise.all(earlierReads)).then(() => route(context, request, options, recipes, policy, registry));
|
|
2911
|
+
barrier = result.then(() => void 0, () => void 0);
|
|
2768
2912
|
return result;
|
|
2769
2913
|
}
|
|
2770
2914
|
};
|
|
2771
2915
|
}
|
|
2772
|
-
|
|
2916
|
+
function isReadTool(tool) {
|
|
2917
|
+
return tool === "sandbox.read" || tool === "sandbox.list" || tool === "sandbox.search" || GRAPH_TOOLS.has(tool);
|
|
2918
|
+
}
|
|
2919
|
+
async function route(context, request, options, recipes, policy, registry) {
|
|
2773
2920
|
try {
|
|
2774
2921
|
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
2775
|
-
if (request.tool === "sandbox.read") return await read(context, request, options, policy);
|
|
2776
|
-
if (request.tool === "sandbox.list") return await list(context, request, options, policy);
|
|
2777
|
-
if (request.tool === "sandbox.search") return await search(context, request, options, policy);
|
|
2778
|
-
if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy);
|
|
2779
|
-
if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
|
|
2922
|
+
if (request.tool === "sandbox.read") return await read(context, request, options, policy, registry);
|
|
2923
|
+
if (request.tool === "sandbox.list") return await list(context, request, options, policy, registry);
|
|
2924
|
+
if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
|
|
2925
|
+
if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
|
|
2926
|
+
if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
|
|
2780
2927
|
return await recipe(context, request, options, recipes, policy);
|
|
2781
2928
|
} catch (reason) {
|
|
2782
2929
|
return response(request, false, toolFailureMessage(reason));
|
|
@@ -2791,7 +2938,7 @@ function toolFailureMessage(reason) {
|
|
|
2791
2938
|
if (code === "EACCES" || code === "EPERM") return "that path is not readable through this tool";
|
|
2792
2939
|
return "tool failed closed";
|
|
2793
2940
|
}
|
|
2794
|
-
async function patch(context, request, options, policy) {
|
|
2941
|
+
async function patch(context, request, options, policy, registry) {
|
|
2795
2942
|
exactKeys(request.input, ["patch"]);
|
|
2796
2943
|
const value = stringField(request.input, "patch");
|
|
2797
2944
|
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
@@ -2801,6 +2948,8 @@ async function patch(context, request, options, policy) {
|
|
|
2801
2948
|
const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
|
|
2802
2949
|
if (!allowed) return response(request, false, "tool denied by CaMeL policy");
|
|
2803
2950
|
await applyCodePatch(context.workspaceDir, value, paths);
|
|
2951
|
+
registry.invalidate(context.workspaceDir);
|
|
2952
|
+
forgetWorkspaceGraphs(context.workspaceDir);
|
|
2804
2953
|
return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
2805
2954
|
}
|
|
2806
2955
|
async function recipe(context, request, options, recipes, policy) {
|