@odla-ai/harness 0.7.1 → 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";
@@ -1838,16 +1838,16 @@ function createCodePolicyGate(options) {
1838
1838
  }
1839
1839
  };
1840
1840
  }
1841
- function directoryPrefixes(paths) {
1841
+ function directoryPrefixes(paths2) {
1842
1842
  const prefixes = /* @__PURE__ */ new Set(["."]);
1843
- for (const path of paths) {
1843
+ for (const path of paths2) {
1844
1844
  const parts = path.split("/");
1845
1845
  for (let index = 1; index < parts.length; index += 1) prefixes.add(parts.slice(0, index).join("/"));
1846
1846
  }
1847
1847
  return [...prefixes].sort();
1848
1848
  }
1849
- async function safePrefix(base, paths, prefix) {
1850
- const prefixes = directoryPrefixes(paths);
1849
+ async function safePrefix(base, paths2, prefix) {
1850
+ const prefixes = directoryPrefixes(paths2);
1851
1851
  const conversions = await conversionRegistry(
1852
1852
  [await registeredPolicy("code.prefix.v1", "code.prefixes.v1", prefixes)],
1853
1853
  { "code.prefixes.v1": prefixes }
@@ -1977,7 +1977,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
1977
1977
  files(root) {
1978
1978
  const existing = cache2.get(root);
1979
1979
  if (existing) return existing;
1980
- const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
1980
+ const pending = enumerate(root, limit).then((paths2) => Object.freeze(paths2));
1981
1981
  cache2.set(root, pending);
1982
1982
  void pending.catch(() => {
1983
1983
  if (cache2.get(root) === pending) cache2.delete(root);
@@ -1990,7 +1990,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
1990
1990
  };
1991
1991
  }
1992
1992
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
1993
- const paths = [];
1993
+ const paths2 = [];
1994
1994
  const walk = async (directory) => {
1995
1995
  for (const entry of await (0, import_promises7.readdir)(directory, { withFileTypes: true })) {
1996
1996
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
@@ -2004,26 +2004,26 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2004
2004
  } catch {
2005
2005
  continue;
2006
2006
  }
2007
- paths.push(path);
2008
- 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");
2009
2009
  }
2010
2010
  }
2011
2011
  };
2012
2012
  await walk((0, import_node_path8.resolve)(root));
2013
- return paths.sort();
2013
+ return paths2.sort();
2014
2014
  }
2015
- function listWorkspace(paths, options = {}) {
2015
+ function listWorkspace(paths2, options = {}) {
2016
2016
  const max = options.maxEntries ?? 1e3;
2017
2017
  const prefix = options.prefix?.replace(/\/+$/, "");
2018
- 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];
2019
2019
  return scoped.slice(0, max);
2020
2020
  }
2021
- async function searchWorkspace(root, paths, options) {
2021
+ async function searchWorkspace(root, paths2, options) {
2022
2022
  options.signal?.throwIfAborted();
2023
2023
  if (!options.query) throw new TypeError("search query must be a non-empty string");
2024
2024
  const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
2025
2025
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
2026
- 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
2027
  if (scoped.length === 0) return [];
2028
2028
  try {
2029
2029
  return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
@@ -2033,11 +2033,11 @@ async function searchWorkspace(root, paths, options) {
2033
2033
  }
2034
2034
  }
2035
2035
  var MAX_NATIVE_ARG_BYTES = 96 * 1024;
2036
- async function nativeSearch(root, paths, options) {
2036
+ async function nativeSearch(root, paths2, options) {
2037
2037
  const batches = [];
2038
2038
  let batch = [];
2039
2039
  let bytes = 0;
2040
- for (const path of paths) {
2040
+ for (const path of paths2) {
2041
2041
  const size = Buffer.byteLength(path) + 1;
2042
2042
  if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
2043
2043
  batches.push(batch);
@@ -2056,7 +2056,7 @@ async function nativeSearch(root, paths, options) {
2056
2056
  }
2057
2057
  return matches;
2058
2058
  }
2059
- function nativeSearchBatch(root, paths, options, remaining) {
2059
+ function nativeSearchBatch(root, paths2, options, remaining) {
2060
2060
  return new Promise((resolveMatches, reject) => {
2061
2061
  const args = [
2062
2062
  "--fixed-strings",
@@ -2069,7 +2069,7 @@ function nativeSearchBatch(root, paths, options, remaining) {
2069
2069
  options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
2070
2070
  "--",
2071
2071
  options.query,
2072
- ...paths
2072
+ ...paths2
2073
2073
  ];
2074
2074
  const child = (0, import_node_child_process5.spawn)("rg", args, {
2075
2075
  cwd: root,
@@ -2146,7 +2146,7 @@ var import_node_path9 = require("path");
2146
2146
  var import_graph = require("@odla-ai/graph");
2147
2147
  var import_code4 = require("@odla-ai/graph/code");
2148
2148
  var cache = /* @__PURE__ */ new Map();
2149
- function workspaceGraphs(workspaceDir, paths) {
2149
+ function workspaceGraphs(workspaceDir, paths2) {
2150
2150
  const existing = cache.get(workspaceDir);
2151
2151
  if (existing) return existing;
2152
2152
  const read2 = (path) => (0, import_promises8.readFile)((0, import_node_path9.join)(workspaceDir, path), "utf8");
@@ -2154,7 +2154,7 @@ function workspaceGraphs(workspaceDir, paths) {
2154
2154
  // No knownTables: a staged workspace may not carry migrations, and a filter
2155
2155
  // that silently drops every table is worse than an unfiltered one. Callers
2156
2156
  // with ground truth should build the graph themselves.
2157
- 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.") } })
2158
2158
  }))();
2159
2159
  cache.set(workspaceDir, built);
2160
2160
  return built;
@@ -2216,11 +2216,11 @@ async function read(context, request, options, policy, registry) {
2216
2216
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
2217
2217
  throw new TypeError("requested line range exceeds its bound");
2218
2218
  }
2219
- const paths = await registry.files(context.workspaceDir);
2220
- if (!paths.includes(path)) {
2219
+ const paths2 = await registry.files(context.workspaceDir);
2220
+ if (!paths2.includes(path)) {
2221
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.`);
2222
2222
  }
2223
- 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 }));
2224
2224
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2225
2225
  const target = resolveCodePath(context.workspaceDir, path);
2226
2226
  const info = await (0, import_promises9.stat)(target);
@@ -2242,16 +2242,16 @@ async function list(context, request, options, policy, registry) {
2242
2242
  const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
2243
2243
  const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
2244
2244
  if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
2245
- const paths = await registry.files(context.workspaceDir);
2246
- 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 } : {} }));
2247
2247
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2248
- const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
2248
+ const entries = listWorkspace(paths2, { ...prefix ? { prefix } : {}, maxEntries });
2249
2249
  if (!entries.length) {
2250
2250
  return response(request, true, prefix ? `No files under "${prefix}".` : "Workspace is empty.", { count: 0 });
2251
2251
  }
2252
- const truncated = entries.length < paths.length && entries.length === maxEntries;
2253
- const hint = !prefix && paths.length > 500 ? `
2254
- \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.` : "";
2255
2255
  return response(
2256
2256
  request,
2257
2257
  true,
@@ -2269,10 +2269,10 @@ async function search(context, request, options, policy, registry) {
2269
2269
  const maxResults = optionalInteger(request.input.maxResults) ?? 100;
2270
2270
  if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
2271
2271
  const caseSensitive = request.input.caseSensitive === void 0 ? true : request.input.caseSensitive === true;
2272
- const paths = await registry.files(context.workspaceDir);
2273
- 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 } : {} }));
2274
2274
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2275
- const matches = await searchWorkspace(context.workspaceDir, paths, {
2275
+ const matches = await searchWorkspace(context.workspaceDir, paths2, {
2276
2276
  query,
2277
2277
  maxResults,
2278
2278
  caseSensitive,
@@ -2294,8 +2294,8 @@ async function graphQuery(context, request, options, policy, registry) {
2294
2294
  selector: query
2295
2295
  }));
2296
2296
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2297
- const paths = await registry.files(context.workspaceDir);
2298
- const graphs = await workspaceGraphs(context.workspaceDir, paths);
2297
+ const paths2 = await registry.files(context.workspaceDir);
2298
+ const graphs = await workspaceGraphs(context.workspaceDir, paths2);
2299
2299
  if (request.tool === "sandbox.overview") {
2300
2300
  return response(request, true, renderOverview(graphs, query || void 0));
2301
2301
  }
@@ -2359,16 +2359,16 @@ function toolFailureMessage(reason) {
2359
2359
  async function patch(context, request, options, policy, registry) {
2360
2360
  exactKeys(request.input, ["patch"]);
2361
2361
  const value = stringField(request.input, "patch");
2362
- const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2363
- 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}/`)))) {
2364
2364
  throw new TypeError("patch targets a read-only reference source");
2365
2365
  }
2366
2366
  const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
2367
2367
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2368
- await applyCodePatch(context.workspaceDir, value, paths);
2368
+ await applyCodePatch(context.workspaceDir, value, paths2);
2369
2369
  registry.invalidate(context.workspaceDir);
2370
2370
  forgetWorkspaceGraphs(context.workspaceDir);
2371
- return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
2371
+ return response(request, true, `Applied patch to ${paths2.length} file(s).`, { paths: paths2 });
2372
2372
  }
2373
2373
  async function recipe(context, request, options, recipes, policy) {
2374
2374
  exactKeys(request.input, ["recipeId"]);
@@ -2729,12 +2729,129 @@ var import_node_crypto4 = require("crypto");
2729
2729
  async function appendCodeRuntimeEvent(control, command, event, refs) {
2730
2730
  const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
2731
2731
  refs.push(eventId);
2732
- 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;
2733
2734
  await control.appendSessionEvent(command.sessionId, eventId, bounded);
2734
2735
  }
2735
2736
  var digestRuntimeValue = (value) => `sha256:${(0, import_node_crypto4.createHash)("sha256").update(value).digest("hex")}`;
2736
2737
  var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
2737
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
+
2738
2855
  // src/code-runtime-engine.ts
2739
2856
  var TheseusRuntimeEngine = class {
2740
2857
  constructor(options) {
@@ -2969,18 +3086,29 @@ var TheseusRuntimeEngine = class {
2969
3086
  return {
2970
3087
  execute: async (context, request) => {
2971
3088
  const startedAt = Date.now();
3089
+ const operationId = digestRuntimeValue(`${command.commandId}:${request.requestId}`);
3090
+ const startedPresentation = codeToolRequestPresentation(request);
2972
3091
  await this.#event(
2973
3092
  command,
2974
- { 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
+ },
2975
3100
  active.conversationRefs
2976
3101
  ).catch(() => void 0);
2977
3102
  const response2 = await broker.execute(context, request);
3103
+ const completedPresentation = codeToolResultPresentation(request, response2);
2978
3104
  await this.#event(command, {
2979
3105
  type: "tool",
2980
3106
  phase: "completed",
2981
3107
  tool: request.tool,
2982
3108
  ok: response2.ok,
2983
- durationMs: Date.now() - startedAt
3109
+ durationMs: Date.now() - startedAt,
3110
+ operationId,
3111
+ ...completedPresentation ? { presentation: completedPresentation } : {}
2984
3112
  }, active.conversationRefs).catch(() => void 0);
2985
3113
  return response2;
2986
3114
  }