@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.
package/dist/node.cjs CHANGED
@@ -323,8 +323,8 @@ async function runContainerAttempt(options) {
323
323
  let stopped = false;
324
324
  let exited = false;
325
325
  child.stderr.setEncoding("utf8");
326
- child.stderr.on("data", (text) => {
327
- if (stderr.length < 64 * 1024) stderr += text.slice(0, 64 * 1024 - stderr.length);
326
+ child.stderr.on("data", (text2) => {
327
+ if (stderr.length < 64 * 1024) stderr += text2.slice(0, 64 * 1024 - stderr.length);
328
328
  });
329
329
  const stop = (reason) => {
330
330
  if (stopped || exited) return;
@@ -497,8 +497,8 @@ async function materializeGitTree(source, commitSha, options = {}) {
497
497
  const maxFiles = options.maxFiles ?? 2e4;
498
498
  const maxBytes = options.maxBytes ?? 512 * 1024 * 1024;
499
499
  const inventory = (await gitOutput(sourceDir, ["ls-tree", "-rz", commitSha], 16 * 1024 * 1024)).toString("utf8").split("\0").filter(Boolean);
500
- const entries = inventory.flatMap((record4) => {
501
- const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record4);
500
+ const entries = inventory.flatMap((record5) => {
501
+ const match = /^(100644|100755) blob ([0-9a-f]{40,64})\t([\s\S]+)$/.exec(record5);
502
502
  return match && allowedWorkspacePath(match[3]) ? [{ mode: match[1], hash: match[2], path: match[3] }] : [];
503
503
  });
504
504
  if (entries.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
@@ -583,12 +583,12 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
583
583
  });
584
584
  if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
585
585
  if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
586
- const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
587
- if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
586
+ const paths2 = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
587
+ if (paths2.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
588
588
  const root = (0, import_node_path4.resolve)(sourceDir);
589
589
  const files = [];
590
590
  let bytes = 0;
591
- for (const relativePath of paths) {
591
+ for (const relativePath of paths2) {
592
592
  if (!allowedWorkspacePath(relativePath)) continue;
593
593
  const source = (0, import_node_path4.resolve)(root, relativePath);
594
594
  if (!source.startsWith(`${root}${import_node_path4.sep}`)) throw new TypeError("git file path escapes workspace");
@@ -796,8 +796,8 @@ async function runLeasedAttempt(lease, options) {
796
796
  allowUnpinnedImage: options.allowUnpinnedImage,
797
797
  workspaceAccess: options.workspaceAccess ?? (options.toolBroker ? "none" : "read-write"),
798
798
  signal: controller.signal,
799
- onStderr: async (text) => {
800
- await append(options.control, lease, "agent.stderr", "agent", { text: text.slice(0, 64 * 1024) });
799
+ onStderr: async (text2) => {
800
+ await append(options.control, lease, "agent.stderr", "agent", { text: text2.slice(0, 64 * 1024) });
801
801
  },
802
802
  onMessage: async (message2) => {
803
803
  if (message2.type === "event") {
@@ -1204,7 +1204,7 @@ function validateCodePatch(rawPatch, maxBytes) {
1204
1204
  if (FORBIDDEN.test(patch2) || /(?:old|new)(?: file)? mode 120000/.test(patch2)) {
1205
1205
  throw new TypeError("patch uses a forbidden binary, link, mode, rename, or copy operation");
1206
1206
  }
1207
- const paths = [];
1207
+ const paths2 = [];
1208
1208
  const lines = patch2.split("\n");
1209
1209
  for (let index = 0; index < lines.length; index += 1) {
1210
1210
  const line = lines[index];
@@ -1220,10 +1220,10 @@ function validateCodePatch(rawPatch, maxBytes) {
1220
1220
  if (!validHeaderPath(oldPath, path, "a") || !validHeaderPath(newPath, path, "b")) {
1221
1221
  throw new TypeError("patch file headers do not match the declared path");
1222
1222
  }
1223
- paths.push(path);
1223
+ paths2.push(path);
1224
1224
  }
1225
- if (!paths.length || new Set(paths).size !== paths.length) throw new TypeError("patch has no diffs or repeats a path");
1226
- return paths;
1225
+ if (!paths2.length || new Set(paths2).size !== paths2.length) throw new TypeError("patch has no diffs or repeats a path");
1226
+ return paths2;
1227
1227
  }
1228
1228
  function validHeaderPath(value, path, prefix) {
1229
1229
  return value === "/dev/null" || value === `${prefix}/${path}`;
@@ -1248,11 +1248,11 @@ function describePatchFailure(patch2, detail) {
1248
1248
  const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
1249
1249
  return `patch did not apply: ${detail}${hint}`;
1250
1250
  }
1251
- async function applyCodePatch(workspaceDir, rawPatch, paths) {
1251
+ async function applyCodePatch(workspaceDir, rawPatch, paths2) {
1252
1252
  const patch2 = stripPatchEnvelope(rawPatch);
1253
1253
  await gitApply(workspaceDir, patch2, true);
1254
1254
  await gitApply(workspaceDir, patch2, false);
1255
- for (const path of paths) {
1255
+ for (const path of paths2) {
1256
1256
  try {
1257
1257
  const info = await (0, import_promises6.lstat)(resolveCodePath(workspaceDir, path));
1258
1258
  if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
@@ -1274,8 +1274,8 @@ function gitApply(cwd, patch2, check) {
1274
1274
  });
1275
1275
  let stderr = "";
1276
1276
  child.stderr.setEncoding("utf8");
1277
- child.stderr.on("data", (text) => {
1278
- if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
1277
+ child.stderr.on("data", (text2) => {
1278
+ if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
1279
1279
  });
1280
1280
  child.once("error", reject);
1281
1281
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr.trim().slice(0, 500)))));
@@ -1301,8 +1301,8 @@ async function restoreCodeWorkspaceCheckpoint(input) {
1301
1301
  const workspace = await stageWorkspace(input.trustedBaseDir, input.stage);
1302
1302
  try {
1303
1303
  if (checkpoint.patch) {
1304
- const paths = validateCodePatch(checkpoint.patch, 256 * 1024);
1305
- await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths);
1304
+ const paths2 = validateCodePatch(checkpoint.patch, 256 * 1024);
1305
+ await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths2);
1306
1306
  }
1307
1307
  return { workspace, checkpoint };
1308
1308
  } catch (error) {
@@ -1471,8 +1471,8 @@ async function verifyCodeCandidate(input) {
1471
1471
  try {
1472
1472
  const baseDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
1473
1473
  if (baseDigest !== input.trustedBaseDigest) throw new TypeError("trusted base does not match its registered digest");
1474
- const paths = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
1475
- await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths);
1474
+ const paths2 = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
1475
+ await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths2);
1476
1476
  const sourceDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
1477
1477
  const policyDigest = digestPolicy(policy);
1478
1478
  const patchDigest = digestBytes(input.candidatePatch);
@@ -1481,7 +1481,7 @@ async function verifyCodeCandidate(input) {
1481
1481
  trustedBaseDigest: input.trustedBaseDigest,
1482
1482
  patchDigest
1483
1483
  });
1484
- const changedTests = changedTestPaths(paths, policy);
1484
+ const changedTests = changedTestPaths(paths2, policy);
1485
1485
  if (changedTests.length > policy.maximumChangedTests) throw new TypeError("candidate changes too many test files");
1486
1486
  const recipes = [];
1487
1487
  const logs = [];
@@ -1548,8 +1548,8 @@ function validate(input) {
1548
1548
  }
1549
1549
  return result;
1550
1550
  }
1551
- function changedTestPaths(paths, policy) {
1552
- return paths.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
1551
+ function changedTestPaths(paths2, policy) {
1552
+ return paths2.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
1553
1553
  }
1554
1554
  function recipeReceipt(recipe2, result, artifacts) {
1555
1555
  const status = result.timedOut ? "timed_out" : result.outputLimitExceeded ? "output_limited" : result.exitCode === 0 && artifacts.every((item) => item.status === "verified") ? "passed" : "failed";
@@ -2420,16 +2420,16 @@ function createCodePolicyGate(options) {
2420
2420
  }
2421
2421
  };
2422
2422
  }
2423
- function directoryPrefixes(paths) {
2423
+ function directoryPrefixes(paths2) {
2424
2424
  const prefixes = /* @__PURE__ */ new Set(["."]);
2425
- for (const path of paths) {
2425
+ for (const path of paths2) {
2426
2426
  const parts = path.split("/");
2427
2427
  for (let index = 1; index < parts.length; index += 1) prefixes.add(parts.slice(0, index).join("/"));
2428
2428
  }
2429
2429
  return [...prefixes].sort();
2430
2430
  }
2431
- async function safePrefix(base, paths, prefix) {
2432
- const prefixes = directoryPrefixes(paths);
2431
+ async function safePrefix(base, paths2, prefix) {
2432
+ const prefixes = directoryPrefixes(paths2);
2433
2433
  const conversions = await conversionRegistry(
2434
2434
  [await registeredPolicy("code.prefix.v1", "code.prefixes.v1", prefixes)],
2435
2435
  { "code.prefixes.v1": prefixes }
@@ -2559,7 +2559,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
2559
2559
  files(root) {
2560
2560
  const existing = cache2.get(root);
2561
2561
  if (existing) return existing;
2562
- const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
2562
+ const pending = enumerate(root, limit).then((paths2) => Object.freeze(paths2));
2563
2563
  cache2.set(root, pending);
2564
2564
  void pending.catch(() => {
2565
2565
  if (cache2.get(root) === pending) cache2.delete(root);
@@ -2572,7 +2572,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
2572
2572
  };
2573
2573
  }
2574
2574
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2575
- const paths = [];
2575
+ const paths2 = [];
2576
2576
  const walk = async (directory) => {
2577
2577
  for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
2578
2578
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
@@ -2586,26 +2586,26 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2586
2586
  } catch {
2587
2587
  continue;
2588
2588
  }
2589
- paths.push(path);
2590
- if (paths.length > limit) throw new TypeError("workspace file registry exceeds its bound");
2589
+ paths2.push(path);
2590
+ if (paths2.length > limit) throw new TypeError("workspace file registry exceeds its bound");
2591
2591
  }
2592
2592
  }
2593
2593
  };
2594
2594
  await walk((0, import_node_path9.resolve)(root));
2595
- return paths.sort();
2595
+ return paths2.sort();
2596
2596
  }
2597
- function listWorkspace(paths, options = {}) {
2597
+ function listWorkspace(paths2, options = {}) {
2598
2598
  const max = options.maxEntries ?? 1e3;
2599
2599
  const prefix = options.prefix?.replace(/\/+$/, "");
2600
- const scoped = prefix ? paths.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths];
2600
+ const scoped = prefix ? paths2.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths2];
2601
2601
  return scoped.slice(0, max);
2602
2602
  }
2603
- async function searchWorkspace(root, paths, options) {
2603
+ async function searchWorkspace(root, paths2, options) {
2604
2604
  options.signal?.throwIfAborted();
2605
2605
  if (!options.query) throw new TypeError("search query must be a non-empty string");
2606
2606
  const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
2607
2607
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
2608
- const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
2608
+ const scoped = listWorkspace(paths2, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths2.length });
2609
2609
  if (scoped.length === 0) return [];
2610
2610
  try {
2611
2611
  return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
@@ -2615,11 +2615,11 @@ async function searchWorkspace(root, paths, options) {
2615
2615
  }
2616
2616
  }
2617
2617
  var MAX_NATIVE_ARG_BYTES = 96 * 1024;
2618
- async function nativeSearch(root, paths, options) {
2618
+ async function nativeSearch(root, paths2, options) {
2619
2619
  const batches = [];
2620
2620
  let batch = [];
2621
2621
  let bytes = 0;
2622
- for (const path of paths) {
2622
+ for (const path of paths2) {
2623
2623
  const size = Buffer.byteLength(path) + 1;
2624
2624
  if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
2625
2625
  batches.push(batch);
@@ -2638,7 +2638,7 @@ async function nativeSearch(root, paths, options) {
2638
2638
  }
2639
2639
  return matches;
2640
2640
  }
2641
- function nativeSearchBatch(root, paths, options, remaining) {
2641
+ function nativeSearchBatch(root, paths2, options, remaining) {
2642
2642
  return new Promise((resolveMatches, reject) => {
2643
2643
  const args = [
2644
2644
  "--fixed-strings",
@@ -2651,7 +2651,7 @@ function nativeSearchBatch(root, paths, options, remaining) {
2651
2651
  options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
2652
2652
  "--",
2653
2653
  options.query,
2654
- ...paths
2654
+ ...paths2
2655
2655
  ];
2656
2656
  const child = (0, import_node_child_process6.spawn)("rg", args, {
2657
2657
  cwd: root,
@@ -2728,7 +2728,7 @@ var import_node_path10 = require("path");
2728
2728
  var import_graph = require("@odla-ai/graph");
2729
2729
  var import_code4 = require("@odla-ai/graph/code");
2730
2730
  var cache = /* @__PURE__ */ new Map();
2731
- function workspaceGraphs(workspaceDir, paths) {
2731
+ function workspaceGraphs(workspaceDir, paths2) {
2732
2732
  const existing = cache.get(workspaceDir);
2733
2733
  if (existing) return existing;
2734
2734
  const read2 = (path) => (0, import_promises10.readFile)((0, import_node_path10.join)(workspaceDir, path), "utf8");
@@ -2736,7 +2736,7 @@ function workspaceGraphs(workspaceDir, paths) {
2736
2736
  // No knownTables: a staged workspace may not carry migrations, and a filter
2737
2737
  // that silently drops every table is worse than an unfiltered one. Callers
2738
2738
  // with ground truth should build the graph themselves.
2739
- graph: await (0, import_code4.buildCodeGraph)({ paths, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2739
+ graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2740
2740
  }))();
2741
2741
  cache.set(workspaceDir, built);
2742
2742
  return built;
@@ -2798,11 +2798,11 @@ async function read(context, request, options, policy, registry) {
2798
2798
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
2799
2799
  throw new TypeError("requested line range exceeds its bound");
2800
2800
  }
2801
- const paths = await registry.files(context.workspaceDir);
2802
- if (!paths.includes(path)) {
2801
+ const paths2 = await registry.files(context.workspaceDir);
2802
+ if (!paths2.includes(path)) {
2803
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.`);
2804
2804
  }
2805
- const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
2805
+ const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
2806
2806
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2807
2807
  const target = resolveCodePath(context.workspaceDir, path);
2808
2808
  const info = await (0, import_promises11.stat)(target);
@@ -2824,16 +2824,16 @@ async function list(context, request, options, policy, registry) {
2824
2824
  const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
2825
2825
  const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
2826
2826
  if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
2827
- const paths = await registry.files(context.workspaceDir);
2828
- const allowed = await policy.list(policyContext(context, request, options, { paths, ...prefix ? { prefix } : {} }));
2827
+ const paths2 = await registry.files(context.workspaceDir);
2828
+ const allowed = await policy.list(policyContext(context, request, options, { paths: paths2, ...prefix ? { prefix } : {} }));
2829
2829
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2830
- const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
2830
+ const entries = listWorkspace(paths2, { ...prefix ? { prefix } : {}, maxEntries });
2831
2831
  if (!entries.length) {
2832
2832
  return response(request, true, prefix ? `No files under "${prefix}".` : "Workspace is empty.", { count: 0 });
2833
2833
  }
2834
- const truncated = entries.length < paths.length && entries.length === maxEntries;
2835
- const hint = !prefix && paths.length > 500 ? `
2836
- \u2026 ${paths.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
2834
+ const truncated = entries.length < paths2.length && entries.length === maxEntries;
2835
+ const hint = !prefix && paths2.length > 500 ? `
2836
+ \u2026 ${paths2.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
2837
2837
  return response(
2838
2838
  request,
2839
2839
  true,
@@ -2851,10 +2851,10 @@ async function search(context, request, options, policy, registry) {
2851
2851
  const maxResults = optionalInteger(request.input.maxResults) ?? 100;
2852
2852
  if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
2853
2853
  const caseSensitive = request.input.caseSensitive === void 0 ? true : request.input.caseSensitive === true;
2854
- const paths = await registry.files(context.workspaceDir);
2855
- const allowed = await policy.search(policyContext(context, request, options, { paths, query, ...prefix ? { prefix } : {} }));
2854
+ const paths2 = await registry.files(context.workspaceDir);
2855
+ const allowed = await policy.search(policyContext(context, request, options, { paths: paths2, query, ...prefix ? { prefix } : {} }));
2856
2856
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2857
- const matches = await searchWorkspace(context.workspaceDir, paths, {
2857
+ const matches = await searchWorkspace(context.workspaceDir, paths2, {
2858
2858
  query,
2859
2859
  maxResults,
2860
2860
  caseSensitive,
@@ -2876,8 +2876,8 @@ async function graphQuery(context, request, options, policy, registry) {
2876
2876
  selector: query
2877
2877
  }));
2878
2878
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2879
- const paths = await registry.files(context.workspaceDir);
2880
- const graphs = await workspaceGraphs(context.workspaceDir, paths);
2879
+ const paths2 = await registry.files(context.workspaceDir);
2880
+ const graphs = await workspaceGraphs(context.workspaceDir, paths2);
2881
2881
  if (request.tool === "sandbox.overview") {
2882
2882
  return response(request, true, renderOverview(graphs, query || void 0));
2883
2883
  }
@@ -2941,16 +2941,16 @@ function toolFailureMessage(reason) {
2941
2941
  async function patch(context, request, options, policy, registry) {
2942
2942
  exactKeys(request.input, ["patch"]);
2943
2943
  const value = stringField(request.input, "patch");
2944
- const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2945
- if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
2944
+ const paths2 = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2945
+ if (paths2.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
2946
2946
  throw new TypeError("patch targets a read-only reference source");
2947
2947
  }
2948
2948
  const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
2949
2949
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2950
- await applyCodePatch(context.workspaceDir, value, paths);
2950
+ await applyCodePatch(context.workspaceDir, value, paths2);
2951
2951
  registry.invalidate(context.workspaceDir);
2952
2952
  forgetWorkspaceGraphs(context.workspaceDir);
2953
- return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
2953
+ return response(request, true, `Applied patch to ${paths2.length} file(s).`, { paths: paths2 });
2954
2954
  }
2955
2955
  async function recipe(context, request, options, recipes, policy) {
2956
2956
  exactKeys(request.input, ["recipeId"]);
@@ -3334,12 +3334,129 @@ var import_node_crypto4 = require("crypto");
3334
3334
  async function appendCodeRuntimeEvent(control, command, event, refs) {
3335
3335
  const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
3336
3336
  refs.push(eventId);
3337
- const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
3337
+ const attributed = { ...event, interactionId: command.commandId };
3338
+ const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
3338
3339
  await control.appendSessionEvent(command.sessionId, eventId, bounded);
3339
3340
  }
3340
3341
  var digestRuntimeValue = (value) => `sha256:${(0, import_node_crypto4.createHash)("sha256").update(value).digest("hex")}`;
3341
3342
  var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
3342
3343
 
3344
+ // src/code-tool-presentation.ts
3345
+ var text = (value, maximum) => {
3346
+ if (typeof value !== "string") return void 0;
3347
+ const bounded = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
3348
+ return bounded ? bounded.slice(0, maximum) : void 0;
3349
+ };
3350
+ var integer2 = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : void 0;
3351
+ var record4 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3352
+ var excerpt = (value, tail = false) => {
3353
+ if (typeof value !== "string") return void 0;
3354
+ const safe = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
3355
+ const source = (tail ? safe.slice(-1e4) : safe.slice(0, 1e4)).trim();
3356
+ if (!source) return void 0;
3357
+ const lines = source.split("\n").filter((line) => line.trim()).map((line) => line.slice(0, 240));
3358
+ const selected = tail ? lines.slice(-10) : lines.slice(0, 10);
3359
+ return text(selected.join("\n"), 2400);
3360
+ };
3361
+ var paths = (value) => {
3362
+ if (!Array.isArray(value)) return void 0;
3363
+ const items = value.flatMap((item) => {
3364
+ const path = text(item, 1024);
3365
+ return path ? [path] : [];
3366
+ }).slice(0, 12);
3367
+ return items.length ? items : void 0;
3368
+ };
3369
+ function patchStats(value) {
3370
+ if (typeof value !== "string") return {};
3371
+ let additions = 0;
3372
+ let deletions = 0;
3373
+ for (const line of value.slice(0, 262144).split("\n")) {
3374
+ if (line.startsWith("+++") || line.startsWith("---")) continue;
3375
+ if (line.startsWith("+")) additions += 1;
3376
+ else if (line.startsWith("-")) deletions += 1;
3377
+ }
3378
+ return { ...additions ? { additions } : {}, ...deletions ? { deletions } : {} };
3379
+ }
3380
+ function searchResults(value) {
3381
+ if (typeof value !== "string") return void 0;
3382
+ const results = value.split("\n").flatMap((line) => {
3383
+ const match = /^([^:\n]{1,1024}):(\d+):\s?(.*)$/.exec(line);
3384
+ if (!match) return [];
3385
+ const lineNumber = Number(match[2]);
3386
+ const itemText = text(match[3], 240);
3387
+ if (!Number.isSafeInteger(lineNumber) || lineNumber < 1) return [];
3388
+ return [{ path: match[1], line: lineNumber, ...itemText ? { text: itemText } : {} }];
3389
+ }).slice(0, 5);
3390
+ return results.length ? results : void 0;
3391
+ }
3392
+ function codeToolRequestPresentation(request) {
3393
+ const input = request.input;
3394
+ if (request.tool === "sandbox.read") {
3395
+ const path = text(input.path, 1024);
3396
+ if (!path) return void 0;
3397
+ const startLine = integer2(input.startLine);
3398
+ const endLine = integer2(input.endLine);
3399
+ return { kind: "read", path, ...startLine ? { startLine } : {}, ...endLine ? { endLine } : {} };
3400
+ }
3401
+ if (request.tool === "sandbox.list") {
3402
+ const scope = text(input.prefix, 1024);
3403
+ return { kind: "list", ...scope ? { scope } : {} };
3404
+ }
3405
+ if (request.tool === "sandbox.search" || request.tool === "sandbox.overview" || request.tool === "sandbox.where_is" || request.tool === "sandbox.who_imports" || request.tool === "sandbox.who_touches") {
3406
+ const query = text(input.query, 512);
3407
+ const scope = request.tool === "sandbox.search" ? text(input.prefix, 1024) : void 0;
3408
+ if (request.tool === "sandbox.search" && !query) return void 0;
3409
+ return { kind: "query", ...query ? { query } : {}, ...scope ? { scope } : {} };
3410
+ }
3411
+ if (request.tool === "sandbox.apply_patch") {
3412
+ return { kind: "patch", ...patchStats(input.patch) };
3413
+ }
3414
+ const recipeId = text(input.recipeId, 120);
3415
+ return recipeId ? { kind: "recipe", recipeId } : void 0;
3416
+ }
3417
+ function codeToolResultPresentation(request, response2) {
3418
+ const started = codeToolRequestPresentation(request);
3419
+ if (!started || !response2.ok) return started;
3420
+ const details = record4(response2.details);
3421
+ if (started.kind === "read") {
3422
+ return {
3423
+ ...started,
3424
+ ...integer2(details?.startLine) ? { startLine: integer2(details?.startLine) } : {},
3425
+ ...integer2(details?.endLine) ? { endLine: integer2(details?.endLine) } : {},
3426
+ ...excerpt(response2.content) ? { excerpt: excerpt(response2.content) } : {}
3427
+ };
3428
+ }
3429
+ if (started.kind === "list") {
3430
+ 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);
3431
+ return {
3432
+ ...started,
3433
+ ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
3434
+ ...listed.length ? { paths: listed } : {}
3435
+ };
3436
+ }
3437
+ if (started.kind === "query") {
3438
+ const results = request.tool === "sandbox.search" ? searchResults(response2.content) : void 0;
3439
+ const resultExcerpt = request.tool === "sandbox.search" ? void 0 : excerpt(response2.content);
3440
+ return {
3441
+ ...started,
3442
+ ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
3443
+ ...results ? { results } : {},
3444
+ ...resultExcerpt ? { excerpt: resultExcerpt } : {}
3445
+ };
3446
+ }
3447
+ if (started.kind === "patch") {
3448
+ return { ...started, ...paths(details?.paths) ? { paths: paths(details?.paths) } : {} };
3449
+ }
3450
+ const output = response2.content.replace(/^Recipe [^\n]*\.?\s*/u, "");
3451
+ return {
3452
+ ...started,
3453
+ ...integer2(details?.exitCode) !== void 0 ? { exitCode: integer2(details?.exitCode) } : {},
3454
+ ...typeof details?.timedOut === "boolean" ? { timedOut: details.timedOut } : {},
3455
+ ...typeof details?.outputLimitExceeded === "boolean" ? { outputLimitExceeded: details.outputLimitExceeded } : {},
3456
+ ...excerpt(output, true) ? { excerpt: excerpt(output, true) } : {}
3457
+ };
3458
+ }
3459
+
3343
3460
  // src/code-runtime-engine.ts
3344
3461
  var TheseusRuntimeEngine = class {
3345
3462
  constructor(options) {
@@ -3574,18 +3691,29 @@ var TheseusRuntimeEngine = class {
3574
3691
  return {
3575
3692
  execute: async (context, request) => {
3576
3693
  const startedAt = Date.now();
3694
+ const operationId = digestRuntimeValue(`${command.commandId}:${request.requestId}`);
3695
+ const startedPresentation = codeToolRequestPresentation(request);
3577
3696
  await this.#event(
3578
3697
  command,
3579
- { type: "tool", phase: "started", tool: request.tool },
3698
+ {
3699
+ type: "tool",
3700
+ phase: "started",
3701
+ tool: request.tool,
3702
+ operationId,
3703
+ ...startedPresentation ? { presentation: startedPresentation } : {}
3704
+ },
3580
3705
  active.conversationRefs
3581
3706
  ).catch(() => void 0);
3582
3707
  const response2 = await broker.execute(context, request);
3708
+ const completedPresentation = codeToolResultPresentation(request, response2);
3583
3709
  await this.#event(command, {
3584
3710
  type: "tool",
3585
3711
  phase: "completed",
3586
3712
  tool: request.tool,
3587
3713
  ok: response2.ok,
3588
- durationMs: Date.now() - startedAt
3714
+ durationMs: Date.now() - startedAt,
3715
+ operationId,
3716
+ ...completedPresentation ? { presentation: completedPresentation } : {}
3589
3717
  }, active.conversationRefs).catch(() => void 0);
3590
3718
  return response2;
3591
3719
  }
@@ -3830,9 +3958,9 @@ var MEASURED_PREMIUM = Object.freeze({
3830
3958
  decomposePerSubGoal: 0.23
3831
3959
  });
3832
3960
  var actionable = (feedback) => {
3833
- const text = feedback.trim();
3834
- if (text.length < 12) return false;
3835
- return /\b(expected|assert|error|fail(?:ed|ure)?|exit|line \d+|\.[a-z]{1,4}:\d+)\b/i.test(text) || /\.(js|ts|tsx|jsx|mjs|cjs|py|go|rs|java|rb)\b/i.test(text);
3961
+ const text2 = feedback.trim();
3962
+ if (text2.length < 12) return false;
3963
+ return /\b(expected|assert|error|fail(?:ed|ure)?|exit|line \d+|\.[a-z]{1,4}:\d+)\b/i.test(text2) || /\.(js|ts|tsx|jsx|mjs|cjs|py|go|rs|java|rb)\b/i.test(text2);
3836
3964
  };
3837
3965
  function chooseStrategy(signals = {}) {
3838
3966
  const width = Math.max(1, signals.width ?? 3);