@odla-ai/harness 0.7.0 → 0.8.0

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.
@@ -317,7 +317,7 @@ function validateCodePatch(rawPatch, maxBytes) {
317
317
  if (FORBIDDEN.test(patch2) || /(?:old|new)(?: file)? mode 120000/.test(patch2)) {
318
318
  throw new TypeError("patch uses a forbidden binary, link, mode, rename, or copy operation");
319
319
  }
320
- const paths = [];
320
+ const paths2 = [];
321
321
  const lines = patch2.split("\n");
322
322
  for (let index = 0; index < lines.length; index += 1) {
323
323
  const line = lines[index];
@@ -333,10 +333,10 @@ function validateCodePatch(rawPatch, maxBytes) {
333
333
  if (!validHeaderPath(oldPath, path, "a") || !validHeaderPath(newPath, path, "b")) {
334
334
  throw new TypeError("patch file headers do not match the declared path");
335
335
  }
336
- paths.push(path);
336
+ paths2.push(path);
337
337
  }
338
- if (!paths.length || new Set(paths).size !== paths.length) throw new TypeError("patch has no diffs or repeats a path");
339
- return paths;
338
+ if (!paths2.length || new Set(paths2).size !== paths2.length) throw new TypeError("patch has no diffs or repeats a path");
339
+ return paths2;
340
340
  }
341
341
  function validHeaderPath(value, path, prefix) {
342
342
  return value === "/dev/null" || value === `${prefix}/${path}`;
@@ -361,11 +361,11 @@ function describePatchFailure(patch2, detail) {
361
361
  const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
362
362
  return `patch did not apply: ${detail}${hint}`;
363
363
  }
364
- async function applyCodePatch(workspaceDir, rawPatch, paths) {
364
+ async function applyCodePatch(workspaceDir, rawPatch, paths2) {
365
365
  const patch2 = stripPatchEnvelope(rawPatch);
366
366
  await gitApply(workspaceDir, patch2, true);
367
367
  await gitApply(workspaceDir, patch2, false);
368
- for (const path of paths) {
368
+ for (const path of paths2) {
369
369
  try {
370
370
  const info = await (0, import_promises.lstat)(resolveCodePath(workspaceDir, path));
371
371
  if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
@@ -387,8 +387,8 @@ function gitApply(cwd, patch2, check) {
387
387
  });
388
388
  let stderr = "";
389
389
  child.stderr.setEncoding("utf8");
390
- child.stderr.on("data", (text) => {
391
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
390
+ child.stderr.on("data", (text2) => {
391
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
392
392
  });
393
393
  child.once("error", reject);
394
394
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr.trim().slice(0, 500)))));
@@ -471,12 +471,12 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
471
471
  });
472
472
  if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
473
473
  if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
474
- const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
475
- if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
474
+ const paths2 = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
475
+ if (paths2.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
476
476
  const root = (0, import_node_path3.resolve)(sourceDir);
477
477
  const files = [];
478
478
  let bytes = 0;
479
- for (const relativePath of paths) {
479
+ for (const relativePath of paths2) {
480
480
  if (!allowedWorkspacePath(relativePath)) continue;
481
481
  const source = (0, import_node_path3.resolve)(root, relativePath);
482
482
  if (!source.startsWith(`${root}${import_node_path3.sep}`)) throw new TypeError("git file path escapes workspace");
@@ -610,8 +610,8 @@ async function restoreCodeWorkspaceCheckpoint(input) {
610
610
  const workspace = await stageWorkspace(input.trustedBaseDir, input.stage);
611
611
  try {
612
612
  if (checkpoint.patch) {
613
- const paths = validateCodePatch(checkpoint.patch, 256 * 1024);
614
- await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths);
613
+ const paths2 = validateCodePatch(checkpoint.patch, 256 * 1024);
614
+ await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths2);
615
615
  }
616
616
  return { workspace, checkpoint };
617
617
  } catch (error) {
@@ -889,8 +889,8 @@ async function verifyCodeCandidate(input) {
889
889
  try {
890
890
  const baseDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
891
891
  if (baseDigest !== input.trustedBaseDigest) throw new TypeError("trusted base does not match its registered digest");
892
- const paths = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
893
- await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths);
892
+ const paths2 = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
893
+ await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths2);
894
894
  const sourceDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
895
895
  const policyDigest = digestPolicy(policy);
896
896
  const patchDigest = digestBytes(input.candidatePatch);
@@ -899,7 +899,7 @@ async function verifyCodeCandidate(input) {
899
899
  trustedBaseDigest: input.trustedBaseDigest,
900
900
  patchDigest
901
901
  });
902
- const changedTests = changedTestPaths(paths, policy);
902
+ const changedTests = changedTestPaths(paths2, policy);
903
903
  if (changedTests.length > policy.maximumChangedTests) throw new TypeError("candidate changes too many test files");
904
904
  const recipes = [];
905
905
  const logs = [];
@@ -966,8 +966,8 @@ function validate(input) {
966
966
  }
967
967
  return result;
968
968
  }
969
- function changedTestPaths(paths, policy) {
970
- return paths.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
969
+ function changedTestPaths(paths2, policy) {
970
+ return paths2.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
971
971
  }
972
972
  function recipeReceipt(recipe2, result, artifacts) {
973
973
  const status = result.timedOut ? "timed_out" : result.outputLimitExceeded ? "output_limited" : result.exitCode === 0 && artifacts.every((item) => item.status === "verified") ? "passed" : "failed";
@@ -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}-${++seq}`, tool, input }
1445
+ { requestId: `bench-${tool}-${sequence}`, tool, input }
1440
1446
  );
1441
- opts.onToolCall?.({
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"] } : {},
@@ -1823,16 +1838,16 @@ function createCodePolicyGate(options) {
1823
1838
  }
1824
1839
  };
1825
1840
  }
1826
- function directoryPrefixes(paths) {
1841
+ function directoryPrefixes(paths2) {
1827
1842
  const prefixes = /* @__PURE__ */ new Set(["."]);
1828
- for (const path of paths) {
1843
+ for (const path of paths2) {
1829
1844
  const parts = path.split("/");
1830
1845
  for (let index = 1; index < parts.length; index += 1) prefixes.add(parts.slice(0, index).join("/"));
1831
1846
  }
1832
1847
  return [...prefixes].sort();
1833
1848
  }
1834
- async function safePrefix(base, paths, prefix) {
1835
- const prefixes = directoryPrefixes(paths);
1849
+ async function safePrefix(base, paths2, prefix) {
1850
+ const prefixes = directoryPrefixes(paths2);
1836
1851
  const conversions = await conversionRegistry(
1837
1852
  [await registeredPolicy("code.prefix.v1", "code.prefixes.v1", prefixes)],
1838
1853
  { "code.prefixes.v1": prefixes }
@@ -1950,13 +1965,32 @@ 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((paths2) => Object.freeze(paths2));
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
- const paths = [];
1993
+ const paths2 = [];
1960
1994
  const walk = async (directory) => {
1961
1995
  for (const entry of await (0, import_promises7.readdir)(directory, { withFileTypes: true })) {
1962
1996
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
@@ -1970,43 +2004,137 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
1970
2004
  } catch {
1971
2005
  continue;
1972
2006
  }
1973
- paths.push(path);
1974
- if (paths.length > limit) throw new TypeError("workspace file registry exceeds its bound");
2007
+ paths2.push(path);
2008
+ if (paths2.length > limit) throw new TypeError("workspace file registry exceeds its bound");
1975
2009
  }
1976
2010
  }
1977
2011
  };
1978
2012
  await walk((0, import_node_path8.resolve)(root));
1979
- return paths.sort();
2013
+ return paths2.sort();
1980
2014
  }
1981
- function listWorkspace(paths, options = {}) {
2015
+ function listWorkspace(paths2, options = {}) {
1982
2016
  const max = options.maxEntries ?? 1e3;
1983
2017
  const prefix = options.prefix?.replace(/\/+$/, "");
1984
- const scoped = prefix ? paths.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths];
2018
+ const scoped = prefix ? paths2.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths2];
1985
2019
  return scoped.slice(0, max);
1986
2020
  }
1987
- async function searchWorkspace(root, paths, options) {
1988
- const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
1989
- if (!query) throw new TypeError("search query must be a non-empty string");
2021
+ async function searchWorkspace(root, paths2, options) {
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
- const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
2026
+ const scoped = listWorkspace(paths2, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths2.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, paths2, options) {
2037
+ const batches = [];
2038
+ let batch = [];
2039
+ let bytes = 0;
2040
+ for (const path of paths2) {
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, paths2, 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
+ ...paths2
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
- if (matches.length >= maxResults) break;
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;
@@ -2018,7 +2146,7 @@ var import_node_path9 = require("path");
2018
2146
  var import_graph = require("@odla-ai/graph");
2019
2147
  var import_code4 = require("@odla-ai/graph/code");
2020
2148
  var cache = /* @__PURE__ */ new Map();
2021
- function workspaceGraphs(workspaceDir, paths) {
2149
+ function workspaceGraphs(workspaceDir, paths2) {
2022
2150
  const existing = cache.get(workspaceDir);
2023
2151
  if (existing) return existing;
2024
2152
  const read2 = (path) => (0, import_promises8.readFile)((0, import_node_path9.join)(workspaceDir, path), "utf8");
@@ -2026,11 +2154,14 @@ function workspaceGraphs(workspaceDir, paths) {
2026
2154
  // No knownTables: a staged workspace may not carry migrations, and a filter
2027
2155
  // that silently drops every table is worse than an unfiltered one. Callers
2028
2156
  // with ground truth should build the graph themselves.
2029
- graph: await (0, import_code4.buildCodeGraph)({ paths, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2157
+ graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2030
2158
  }))();
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,11 +2216,11 @@ 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 registeredFiles(context.workspaceDir, 2e4);
2089
- if (!paths.includes(path)) {
2219
+ const paths2 = await registry.files(context.workspaceDir);
2220
+ if (!paths2.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
  }
2092
- const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
2223
+ const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
2093
2224
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2094
2225
  const target = resolveCodePath(context.workspaceDir, path);
2095
2226
  const info = await (0, import_promises9.stat)(target);
@@ -2105,22 +2236,22 @@ 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 registeredFiles(context.workspaceDir, 2e4);
2115
- const allowed = await policy.list(policyContext(context, request, options, { paths, ...prefix ? { prefix } : {} }));
2245
+ const paths2 = await registry.files(context.workspaceDir);
2246
+ const allowed = await policy.list(policyContext(context, request, options, { paths: paths2, ...prefix ? { prefix } : {} }));
2116
2247
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2117
- const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
2248
+ const entries = listWorkspace(paths2, { ...prefix ? { prefix } : {}, maxEntries });
2118
2249
  if (!entries.length) {
2119
2250
  return response(request, true, prefix ? `No files under "${prefix}".` : "Workspace is empty.", { count: 0 });
2120
2251
  }
2121
- const truncated = entries.length < paths.length && entries.length === maxEntries;
2122
- const hint = !prefix && paths.length > 500 ? `
2123
- \u2026 ${paths.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
2252
+ const truncated = entries.length < paths2.length && entries.length === maxEntries;
2253
+ const hint = !prefix && paths2.length > 500 ? `
2254
+ \u2026 ${paths2.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
2124
2255
  return response(
2125
2256
  request,
2126
2257
  true,
@@ -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 registeredFiles(context.workspaceDir, 2e4);
2142
- const allowed = await policy.search(policyContext(context, request, options, { paths, query, ...prefix ? { prefix } : {} }));
2272
+ const paths2 = await registry.files(context.workspaceDir);
2273
+ const allowed = await policy.search(policyContext(context, request, options, { paths: paths2, query, ...prefix ? { prefix } : {} }));
2143
2274
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2144
- const matches = await searchWorkspace(context.workspaceDir, paths, {
2275
+ const matches = await searchWorkspace(context.workspaceDir, paths2, {
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,8 +2294,8 @@ 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 registeredFiles(context.workspaceDir, 2e4);
2166
- const graphs = await workspaceGraphs(context.workspaceDir, paths);
2297
+ const paths2 = await registry.files(context.workspaceDir);
2298
+ const graphs = await workspaceGraphs(context.workspaceDir, paths2);
2167
2299
  if (request.tool === "sandbox.overview") {
2168
2300
  return response(request, true, renderOverview(graphs, query || void 0));
2169
2301
  }
@@ -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
- let tail = Promise.resolve();
2313
+ const registry = createWorkspaceFileRegistry();
2314
+ let barrier = Promise.resolve();
2315
+ const activeReads = /* @__PURE__ */ new Set();
2182
2316
  return {
2183
2317
  execute(context, request) {
2184
- const result = tail.then(() => route(context, request, options, recipes, policy));
2185
- tail = result.then(() => void 0, () => void 0);
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
- async function route(context, request, options, recipes, policy) {
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,17 +2356,19 @@ 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
- const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2216
- if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
2362
+ const paths2 = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2363
+ if (paths2.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
2217
2364
  throw new TypeError("patch targets a read-only reference source");
2218
2365
  }
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
- await applyCodePatch(context.workspaceDir, value, paths);
2222
- return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
2368
+ await applyCodePatch(context.workspaceDir, value, paths2);
2369
+ registry.invalidate(context.workspaceDir);
2370
+ forgetWorkspaceGraphs(context.workspaceDir);
2371
+ return response(request, true, `Applied patch to ${paths2.length} file(s).`, { paths: paths2 });
2223
2372
  }
2224
2373
  async function recipe(context, request, options, recipes, policy) {
2225
2374
  exactKeys(request.input, ["recipeId"]);
@@ -2580,12 +2729,129 @@ var import_node_crypto4 = require("crypto");
2580
2729
  async function appendCodeRuntimeEvent(control, command, event, refs) {
2581
2730
  const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
2582
2731
  refs.push(eventId);
2583
- const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
2732
+ const attributed = { ...event, interactionId: command.commandId };
2733
+ const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
2584
2734
  await control.appendSessionEvent(command.sessionId, eventId, bounded);
2585
2735
  }
2586
2736
  var digestRuntimeValue = (value) => `sha256:${(0, import_node_crypto4.createHash)("sha256").update(value).digest("hex")}`;
2587
2737
  var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
2588
2738
 
2739
+ // src/code-tool-presentation.ts
2740
+ var text = (value, maximum) => {
2741
+ if (typeof value !== "string") return void 0;
2742
+ const bounded = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
2743
+ return bounded ? bounded.slice(0, maximum) : void 0;
2744
+ };
2745
+ var integer2 = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : void 0;
2746
+ var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2747
+ var excerpt = (value, tail = false) => {
2748
+ if (typeof value !== "string") return void 0;
2749
+ const safe = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
2750
+ const source = (tail ? safe.slice(-1e4) : safe.slice(0, 1e4)).trim();
2751
+ if (!source) return void 0;
2752
+ const lines = source.split("\n").filter((line) => line.trim()).map((line) => line.slice(0, 240));
2753
+ const selected = tail ? lines.slice(-10) : lines.slice(0, 10);
2754
+ return text(selected.join("\n"), 2400);
2755
+ };
2756
+ var paths = (value) => {
2757
+ if (!Array.isArray(value)) return void 0;
2758
+ const items = value.flatMap((item) => {
2759
+ const path = text(item, 1024);
2760
+ return path ? [path] : [];
2761
+ }).slice(0, 12);
2762
+ return items.length ? items : void 0;
2763
+ };
2764
+ function patchStats(value) {
2765
+ if (typeof value !== "string") return {};
2766
+ let additions = 0;
2767
+ let deletions = 0;
2768
+ for (const line of value.slice(0, 262144).split("\n")) {
2769
+ if (line.startsWith("+++") || line.startsWith("---")) continue;
2770
+ if (line.startsWith("+")) additions += 1;
2771
+ else if (line.startsWith("-")) deletions += 1;
2772
+ }
2773
+ return { ...additions ? { additions } : {}, ...deletions ? { deletions } : {} };
2774
+ }
2775
+ function searchResults(value) {
2776
+ if (typeof value !== "string") return void 0;
2777
+ const results = value.split("\n").flatMap((line) => {
2778
+ const match = /^([^:\n]{1,1024}):(\d+):\s?(.*)$/.exec(line);
2779
+ if (!match) return [];
2780
+ const lineNumber = Number(match[2]);
2781
+ const itemText = text(match[3], 240);
2782
+ if (!Number.isSafeInteger(lineNumber) || lineNumber < 1) return [];
2783
+ return [{ path: match[1], line: lineNumber, ...itemText ? { text: itemText } : {} }];
2784
+ }).slice(0, 5);
2785
+ return results.length ? results : void 0;
2786
+ }
2787
+ function codeToolRequestPresentation(request) {
2788
+ const input = request.input;
2789
+ if (request.tool === "sandbox.read") {
2790
+ const path = text(input.path, 1024);
2791
+ if (!path) return void 0;
2792
+ const startLine = integer2(input.startLine);
2793
+ const endLine = integer2(input.endLine);
2794
+ return { kind: "read", path, ...startLine ? { startLine } : {}, ...endLine ? { endLine } : {} };
2795
+ }
2796
+ if (request.tool === "sandbox.list") {
2797
+ const scope = text(input.prefix, 1024);
2798
+ return { kind: "list", ...scope ? { scope } : {} };
2799
+ }
2800
+ if (request.tool === "sandbox.search" || request.tool === "sandbox.overview" || request.tool === "sandbox.where_is" || request.tool === "sandbox.who_imports" || request.tool === "sandbox.who_touches") {
2801
+ const query = text(input.query, 512);
2802
+ const scope = request.tool === "sandbox.search" ? text(input.prefix, 1024) : void 0;
2803
+ if (request.tool === "sandbox.search" && !query) return void 0;
2804
+ return { kind: "query", ...query ? { query } : {}, ...scope ? { scope } : {} };
2805
+ }
2806
+ if (request.tool === "sandbox.apply_patch") {
2807
+ return { kind: "patch", ...patchStats(input.patch) };
2808
+ }
2809
+ const recipeId = text(input.recipeId, 120);
2810
+ return recipeId ? { kind: "recipe", recipeId } : void 0;
2811
+ }
2812
+ function codeToolResultPresentation(request, response2) {
2813
+ const started = codeToolRequestPresentation(request);
2814
+ if (!started || !response2.ok) return started;
2815
+ const details = record3(response2.details);
2816
+ if (started.kind === "read") {
2817
+ return {
2818
+ ...started,
2819
+ ...integer2(details?.startLine) ? { startLine: integer2(details?.startLine) } : {},
2820
+ ...integer2(details?.endLine) ? { endLine: integer2(details?.endLine) } : {},
2821
+ ...excerpt(response2.content) ? { excerpt: excerpt(response2.content) } : {}
2822
+ };
2823
+ }
2824
+ if (started.kind === "list") {
2825
+ const listed = response2.content.split("\n").filter((line) => line && !line.startsWith("\u2026") && !line.startsWith("Workspace ")).map((line) => text(line, 1024)).filter((line) => Boolean(line)).slice(0, 8);
2826
+ return {
2827
+ ...started,
2828
+ ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
2829
+ ...listed.length ? { paths: listed } : {}
2830
+ };
2831
+ }
2832
+ if (started.kind === "query") {
2833
+ const results = request.tool === "sandbox.search" ? searchResults(response2.content) : void 0;
2834
+ const resultExcerpt = request.tool === "sandbox.search" ? void 0 : excerpt(response2.content);
2835
+ return {
2836
+ ...started,
2837
+ ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
2838
+ ...results ? { results } : {},
2839
+ ...resultExcerpt ? { excerpt: resultExcerpt } : {}
2840
+ };
2841
+ }
2842
+ if (started.kind === "patch") {
2843
+ return { ...started, ...paths(details?.paths) ? { paths: paths(details?.paths) } : {} };
2844
+ }
2845
+ const output = response2.content.replace(/^Recipe [^\n]*\.?\s*/u, "");
2846
+ return {
2847
+ ...started,
2848
+ ...integer2(details?.exitCode) !== void 0 ? { exitCode: integer2(details?.exitCode) } : {},
2849
+ ...typeof details?.timedOut === "boolean" ? { timedOut: details.timedOut } : {},
2850
+ ...typeof details?.outputLimitExceeded === "boolean" ? { outputLimitExceeded: details.outputLimitExceeded } : {},
2851
+ ...excerpt(output, true) ? { excerpt: excerpt(output, true) } : {}
2852
+ };
2853
+ }
2854
+
2589
2855
  // src/code-runtime-engine.ts
2590
2856
  var TheseusRuntimeEngine = class {
2591
2857
  constructor(options) {
@@ -2820,18 +3086,29 @@ var TheseusRuntimeEngine = class {
2820
3086
  return {
2821
3087
  execute: async (context, request) => {
2822
3088
  const startedAt = Date.now();
3089
+ const operationId = digestRuntimeValue(`${command.commandId}:${request.requestId}`);
3090
+ const startedPresentation = codeToolRequestPresentation(request);
2823
3091
  await this.#event(
2824
3092
  command,
2825
- { type: "tool", phase: "started", tool: request.tool },
3093
+ {
3094
+ type: "tool",
3095
+ phase: "started",
3096
+ tool: request.tool,
3097
+ operationId,
3098
+ ...startedPresentation ? { presentation: startedPresentation } : {}
3099
+ },
2826
3100
  active.conversationRefs
2827
3101
  ).catch(() => void 0);
2828
3102
  const response2 = await broker.execute(context, request);
3103
+ const completedPresentation = codeToolResultPresentation(request, response2);
2829
3104
  await this.#event(command, {
2830
3105
  type: "tool",
2831
3106
  phase: "completed",
2832
3107
  tool: request.tool,
2833
3108
  ok: response2.ok,
2834
- durationMs: Date.now() - startedAt
3109
+ durationMs: Date.now() - startedAt,
3110
+ operationId,
3111
+ ...completedPresentation ? { presentation: completedPresentation } : {}
2835
3112
  }, active.conversationRefs).catch(() => void 0);
2836
3113
  return response2;
2837
3114
  }