@odla-ai/harness 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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";
@@ -2208,6 +2208,7 @@ async function runCodeAgentAttempt(options) {
2208
2208
  model: "brokered",
2209
2209
  surface,
2210
2210
  ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
2211
+ ...options.extraSkills ? { extraSkills: options.extraSkills } : {},
2211
2212
  ...options.maxSteps === void 0 ? {} : { maxSteps: options.maxSteps },
2212
2213
  ...options.budget ? { budget: options.budget } : {},
2213
2214
  ...options.signal ? { signal: options.signal } : {},
@@ -2239,6 +2240,18 @@ Finish with a concise, non-empty answer to the owner. Do not call tools or promi
2239
2240
  }
2240
2241
  }
2241
2242
 
2243
+ // src/code-runtime-session-skills.ts
2244
+ async function sessionSkillsFor(options, command) {
2245
+ try {
2246
+ return await options.sessionSkills?.(command) ?? [];
2247
+ } catch (cause) {
2248
+ options.onDiagnostic?.(
2249
+ `session skills unavailable, continuing with code tools only: ${cause instanceof Error ? cause.message : String(cause)}`
2250
+ );
2251
+ return [];
2252
+ }
2253
+ }
2254
+
2242
2255
  // src/code-runtime-inference.ts
2243
2256
  async function handleCodeRuntimeInference(input) {
2244
2257
  const { command, request, state } = input;
@@ -2420,16 +2433,16 @@ function createCodePolicyGate(options) {
2420
2433
  }
2421
2434
  };
2422
2435
  }
2423
- function directoryPrefixes(paths) {
2436
+ function directoryPrefixes(paths2) {
2424
2437
  const prefixes = /* @__PURE__ */ new Set(["."]);
2425
- for (const path of paths) {
2438
+ for (const path of paths2) {
2426
2439
  const parts = path.split("/");
2427
2440
  for (let index = 1; index < parts.length; index += 1) prefixes.add(parts.slice(0, index).join("/"));
2428
2441
  }
2429
2442
  return [...prefixes].sort();
2430
2443
  }
2431
- async function safePrefix(base, paths, prefix) {
2432
- const prefixes = directoryPrefixes(paths);
2444
+ async function safePrefix(base, paths2, prefix) {
2445
+ const prefixes = directoryPrefixes(paths2);
2433
2446
  const conversions = await conversionRegistry(
2434
2447
  [await registeredPolicy("code.prefix.v1", "code.prefixes.v1", prefixes)],
2435
2448
  { "code.prefixes.v1": prefixes }
@@ -2559,7 +2572,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
2559
2572
  files(root) {
2560
2573
  const existing = cache2.get(root);
2561
2574
  if (existing) return existing;
2562
- const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
2575
+ const pending = enumerate(root, limit).then((paths2) => Object.freeze(paths2));
2563
2576
  cache2.set(root, pending);
2564
2577
  void pending.catch(() => {
2565
2578
  if (cache2.get(root) === pending) cache2.delete(root);
@@ -2572,7 +2585,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
2572
2585
  };
2573
2586
  }
2574
2587
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2575
- const paths = [];
2588
+ const paths2 = [];
2576
2589
  const walk = async (directory) => {
2577
2590
  for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
2578
2591
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
@@ -2586,26 +2599,26 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2586
2599
  } catch {
2587
2600
  continue;
2588
2601
  }
2589
- paths.push(path);
2590
- if (paths.length > limit) throw new TypeError("workspace file registry exceeds its bound");
2602
+ paths2.push(path);
2603
+ if (paths2.length > limit) throw new TypeError("workspace file registry exceeds its bound");
2591
2604
  }
2592
2605
  }
2593
2606
  };
2594
2607
  await walk((0, import_node_path9.resolve)(root));
2595
- return paths.sort();
2608
+ return paths2.sort();
2596
2609
  }
2597
- function listWorkspace(paths, options = {}) {
2610
+ function listWorkspace(paths2, options = {}) {
2598
2611
  const max = options.maxEntries ?? 1e3;
2599
2612
  const prefix = options.prefix?.replace(/\/+$/, "");
2600
- const scoped = prefix ? paths.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths];
2613
+ const scoped = prefix ? paths2.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths2];
2601
2614
  return scoped.slice(0, max);
2602
2615
  }
2603
- async function searchWorkspace(root, paths, options) {
2616
+ async function searchWorkspace(root, paths2, options) {
2604
2617
  options.signal?.throwIfAborted();
2605
2618
  if (!options.query) throw new TypeError("search query must be a non-empty string");
2606
2619
  const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
2607
2620
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
2608
- const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
2621
+ const scoped = listWorkspace(paths2, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths2.length });
2609
2622
  if (scoped.length === 0) return [];
2610
2623
  try {
2611
2624
  return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
@@ -2615,11 +2628,11 @@ async function searchWorkspace(root, paths, options) {
2615
2628
  }
2616
2629
  }
2617
2630
  var MAX_NATIVE_ARG_BYTES = 96 * 1024;
2618
- async function nativeSearch(root, paths, options) {
2631
+ async function nativeSearch(root, paths2, options) {
2619
2632
  const batches = [];
2620
2633
  let batch = [];
2621
2634
  let bytes = 0;
2622
- for (const path of paths) {
2635
+ for (const path of paths2) {
2623
2636
  const size = Buffer.byteLength(path) + 1;
2624
2637
  if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
2625
2638
  batches.push(batch);
@@ -2638,7 +2651,7 @@ async function nativeSearch(root, paths, options) {
2638
2651
  }
2639
2652
  return matches;
2640
2653
  }
2641
- function nativeSearchBatch(root, paths, options, remaining) {
2654
+ function nativeSearchBatch(root, paths2, options, remaining) {
2642
2655
  return new Promise((resolveMatches, reject) => {
2643
2656
  const args = [
2644
2657
  "--fixed-strings",
@@ -2651,7 +2664,7 @@ function nativeSearchBatch(root, paths, options, remaining) {
2651
2664
  options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
2652
2665
  "--",
2653
2666
  options.query,
2654
- ...paths
2667
+ ...paths2
2655
2668
  ];
2656
2669
  const child = (0, import_node_child_process6.spawn)("rg", args, {
2657
2670
  cwd: root,
@@ -2728,7 +2741,7 @@ var import_node_path10 = require("path");
2728
2741
  var import_graph = require("@odla-ai/graph");
2729
2742
  var import_code4 = require("@odla-ai/graph/code");
2730
2743
  var cache = /* @__PURE__ */ new Map();
2731
- function workspaceGraphs(workspaceDir, paths) {
2744
+ function workspaceGraphs(workspaceDir, paths2) {
2732
2745
  const existing = cache.get(workspaceDir);
2733
2746
  if (existing) return existing;
2734
2747
  const read2 = (path) => (0, import_promises10.readFile)((0, import_node_path10.join)(workspaceDir, path), "utf8");
@@ -2736,7 +2749,7 @@ function workspaceGraphs(workspaceDir, paths) {
2736
2749
  // No knownTables: a staged workspace may not carry migrations, and a filter
2737
2750
  // that silently drops every table is worse than an unfiltered one. Callers
2738
2751
  // with ground truth should build the graph themselves.
2739
- graph: await (0, import_code4.buildCodeGraph)({ paths, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2752
+ graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2740
2753
  }))();
2741
2754
  cache.set(workspaceDir, built);
2742
2755
  return built;
@@ -2798,11 +2811,11 @@ async function read(context, request, options, policy, registry) {
2798
2811
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
2799
2812
  throw new TypeError("requested line range exceeds its bound");
2800
2813
  }
2801
- const paths = await registry.files(context.workspaceDir);
2802
- if (!paths.includes(path)) {
2814
+ const paths2 = await registry.files(context.workspaceDir);
2815
+ if (!paths2.includes(path)) {
2803
2816
  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
2817
  }
2805
- const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
2818
+ const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
2806
2819
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2807
2820
  const target = resolveCodePath(context.workspaceDir, path);
2808
2821
  const info = await (0, import_promises11.stat)(target);
@@ -2824,16 +2837,16 @@ async function list(context, request, options, policy, registry) {
2824
2837
  const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
2825
2838
  const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
2826
2839
  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 } : {} }));
2840
+ const paths2 = await registry.files(context.workspaceDir);
2841
+ const allowed = await policy.list(policyContext(context, request, options, { paths: paths2, ...prefix ? { prefix } : {} }));
2829
2842
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2830
- const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
2843
+ const entries = listWorkspace(paths2, { ...prefix ? { prefix } : {}, maxEntries });
2831
2844
  if (!entries.length) {
2832
2845
  return response(request, true, prefix ? `No files under "${prefix}".` : "Workspace is empty.", { count: 0 });
2833
2846
  }
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.` : "";
2847
+ const truncated = entries.length < paths2.length && entries.length === maxEntries;
2848
+ const hint = !prefix && paths2.length > 500 ? `
2849
+ \u2026 ${paths2.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
2837
2850
  return response(
2838
2851
  request,
2839
2852
  true,
@@ -2851,10 +2864,10 @@ async function search(context, request, options, policy, registry) {
2851
2864
  const maxResults = optionalInteger(request.input.maxResults) ?? 100;
2852
2865
  if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
2853
2866
  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 } : {} }));
2867
+ const paths2 = await registry.files(context.workspaceDir);
2868
+ const allowed = await policy.search(policyContext(context, request, options, { paths: paths2, query, ...prefix ? { prefix } : {} }));
2856
2869
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2857
- const matches = await searchWorkspace(context.workspaceDir, paths, {
2870
+ const matches = await searchWorkspace(context.workspaceDir, paths2, {
2858
2871
  query,
2859
2872
  maxResults,
2860
2873
  caseSensitive,
@@ -2876,8 +2889,8 @@ async function graphQuery(context, request, options, policy, registry) {
2876
2889
  selector: query
2877
2890
  }));
2878
2891
  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);
2892
+ const paths2 = await registry.files(context.workspaceDir);
2893
+ const graphs = await workspaceGraphs(context.workspaceDir, paths2);
2881
2894
  if (request.tool === "sandbox.overview") {
2882
2895
  return response(request, true, renderOverview(graphs, query || void 0));
2883
2896
  }
@@ -2941,16 +2954,16 @@ function toolFailureMessage(reason) {
2941
2954
  async function patch(context, request, options, policy, registry) {
2942
2955
  exactKeys(request.input, ["patch"]);
2943
2956
  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}/`)))) {
2957
+ const paths2 = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2958
+ if (paths2.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
2946
2959
  throw new TypeError("patch targets a read-only reference source");
2947
2960
  }
2948
2961
  const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
2949
2962
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2950
- await applyCodePatch(context.workspaceDir, value, paths);
2963
+ await applyCodePatch(context.workspaceDir, value, paths2);
2951
2964
  registry.invalidate(context.workspaceDir);
2952
2965
  forgetWorkspaceGraphs(context.workspaceDir);
2953
- return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
2966
+ return response(request, true, `Applied patch to ${paths2.length} file(s).`, { paths: paths2 });
2954
2967
  }
2955
2968
  async function recipe(context, request, options, recipes, policy) {
2956
2969
  exactKeys(request.input, ["recipeId"]);
@@ -3334,12 +3347,129 @@ var import_node_crypto4 = require("crypto");
3334
3347
  async function appendCodeRuntimeEvent(control, command, event, refs) {
3335
3348
  const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
3336
3349
  refs.push(eventId);
3337
- const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
3350
+ const attributed = { ...event, interactionId: command.commandId };
3351
+ const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
3338
3352
  await control.appendSessionEvent(command.sessionId, eventId, bounded);
3339
3353
  }
3340
3354
  var digestRuntimeValue = (value) => `sha256:${(0, import_node_crypto4.createHash)("sha256").update(value).digest("hex")}`;
3341
3355
  var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
3342
3356
 
3357
+ // src/code-tool-presentation.ts
3358
+ var text = (value, maximum) => {
3359
+ if (typeof value !== "string") return void 0;
3360
+ const bounded = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
3361
+ return bounded ? bounded.slice(0, maximum) : void 0;
3362
+ };
3363
+ var integer2 = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : void 0;
3364
+ var record4 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3365
+ var excerpt = (value, tail = false) => {
3366
+ if (typeof value !== "string") return void 0;
3367
+ const safe = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
3368
+ const source = (tail ? safe.slice(-1e4) : safe.slice(0, 1e4)).trim();
3369
+ if (!source) return void 0;
3370
+ const lines = source.split("\n").filter((line) => line.trim()).map((line) => line.slice(0, 240));
3371
+ const selected = tail ? lines.slice(-10) : lines.slice(0, 10);
3372
+ return text(selected.join("\n"), 2400);
3373
+ };
3374
+ var paths = (value) => {
3375
+ if (!Array.isArray(value)) return void 0;
3376
+ const items = value.flatMap((item) => {
3377
+ const path = text(item, 1024);
3378
+ return path ? [path] : [];
3379
+ }).slice(0, 12);
3380
+ return items.length ? items : void 0;
3381
+ };
3382
+ function patchStats(value) {
3383
+ if (typeof value !== "string") return {};
3384
+ let additions = 0;
3385
+ let deletions = 0;
3386
+ for (const line of value.slice(0, 262144).split("\n")) {
3387
+ if (line.startsWith("+++") || line.startsWith("---")) continue;
3388
+ if (line.startsWith("+")) additions += 1;
3389
+ else if (line.startsWith("-")) deletions += 1;
3390
+ }
3391
+ return { ...additions ? { additions } : {}, ...deletions ? { deletions } : {} };
3392
+ }
3393
+ function searchResults(value) {
3394
+ if (typeof value !== "string") return void 0;
3395
+ const results = value.split("\n").flatMap((line) => {
3396
+ const match = /^([^:\n]{1,1024}):(\d+):\s?(.*)$/.exec(line);
3397
+ if (!match) return [];
3398
+ const lineNumber = Number(match[2]);
3399
+ const itemText = text(match[3], 240);
3400
+ if (!Number.isSafeInteger(lineNumber) || lineNumber < 1) return [];
3401
+ return [{ path: match[1], line: lineNumber, ...itemText ? { text: itemText } : {} }];
3402
+ }).slice(0, 5);
3403
+ return results.length ? results : void 0;
3404
+ }
3405
+ function codeToolRequestPresentation(request) {
3406
+ const input = request.input;
3407
+ if (request.tool === "sandbox.read") {
3408
+ const path = text(input.path, 1024);
3409
+ if (!path) return void 0;
3410
+ const startLine = integer2(input.startLine);
3411
+ const endLine = integer2(input.endLine);
3412
+ return { kind: "read", path, ...startLine ? { startLine } : {}, ...endLine ? { endLine } : {} };
3413
+ }
3414
+ if (request.tool === "sandbox.list") {
3415
+ const scope = text(input.prefix, 1024);
3416
+ return { kind: "list", ...scope ? { scope } : {} };
3417
+ }
3418
+ if (request.tool === "sandbox.search" || request.tool === "sandbox.overview" || request.tool === "sandbox.where_is" || request.tool === "sandbox.who_imports" || request.tool === "sandbox.who_touches") {
3419
+ const query = text(input.query, 512);
3420
+ const scope = request.tool === "sandbox.search" ? text(input.prefix, 1024) : void 0;
3421
+ if (request.tool === "sandbox.search" && !query) return void 0;
3422
+ return { kind: "query", ...query ? { query } : {}, ...scope ? { scope } : {} };
3423
+ }
3424
+ if (request.tool === "sandbox.apply_patch") {
3425
+ return { kind: "patch", ...patchStats(input.patch) };
3426
+ }
3427
+ const recipeId = text(input.recipeId, 120);
3428
+ return recipeId ? { kind: "recipe", recipeId } : void 0;
3429
+ }
3430
+ function codeToolResultPresentation(request, response2) {
3431
+ const started = codeToolRequestPresentation(request);
3432
+ if (!started || !response2.ok) return started;
3433
+ const details = record4(response2.details);
3434
+ if (started.kind === "read") {
3435
+ return {
3436
+ ...started,
3437
+ ...integer2(details?.startLine) ? { startLine: integer2(details?.startLine) } : {},
3438
+ ...integer2(details?.endLine) ? { endLine: integer2(details?.endLine) } : {},
3439
+ ...excerpt(response2.content) ? { excerpt: excerpt(response2.content) } : {}
3440
+ };
3441
+ }
3442
+ if (started.kind === "list") {
3443
+ 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);
3444
+ return {
3445
+ ...started,
3446
+ ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
3447
+ ...listed.length ? { paths: listed } : {}
3448
+ };
3449
+ }
3450
+ if (started.kind === "query") {
3451
+ const results = request.tool === "sandbox.search" ? searchResults(response2.content) : void 0;
3452
+ const resultExcerpt = request.tool === "sandbox.search" ? void 0 : excerpt(response2.content);
3453
+ return {
3454
+ ...started,
3455
+ ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
3456
+ ...results ? { results } : {},
3457
+ ...resultExcerpt ? { excerpt: resultExcerpt } : {}
3458
+ };
3459
+ }
3460
+ if (started.kind === "patch") {
3461
+ return { ...started, ...paths(details?.paths) ? { paths: paths(details?.paths) } : {} };
3462
+ }
3463
+ const output = response2.content.replace(/^Recipe [^\n]*\.?\s*/u, "");
3464
+ return {
3465
+ ...started,
3466
+ ...integer2(details?.exitCode) !== void 0 ? { exitCode: integer2(details?.exitCode) } : {},
3467
+ ...typeof details?.timedOut === "boolean" ? { timedOut: details.timedOut } : {},
3468
+ ...typeof details?.outputLimitExceeded === "boolean" ? { outputLimitExceeded: details.outputLimitExceeded } : {},
3469
+ ...excerpt(output, true) ? { excerpt: excerpt(output, true) } : {}
3470
+ };
3471
+ }
3472
+
3343
3473
  // src/code-runtime-engine.ts
3344
3474
  var TheseusRuntimeEngine = class {
3345
3475
  constructor(options) {
@@ -3534,6 +3664,7 @@ var TheseusRuntimeEngine = class {
3534
3664
  event: (event) => this.#event(command, event, active.conversationRefs)
3535
3665
  });
3536
3666
  await this.#event(command, { type: "status", status: "running" }, active.conversationRefs);
3667
+ const extraSkills = await sessionSkillsFor(this.options, command);
3537
3668
  const result = await this.#attempt({
3538
3669
  inference,
3539
3670
  broker,
@@ -3541,7 +3672,8 @@ var TheseusRuntimeEngine = class {
3541
3672
  workspaceDir: active.workspace.workspaceDir,
3542
3673
  prompt: metadata.prompt,
3543
3674
  signal: active.abort.signal,
3544
- recipeIds: this.options.recipes.map((recipe2) => recipe2.id)
3675
+ recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
3676
+ ...extraSkills.length ? { extraSkills } : {}
3545
3677
  });
3546
3678
  const closing = result.finalText.trim();
3547
3679
  const completed = result.status === "completed" && Boolean(closing);
@@ -3574,18 +3706,29 @@ var TheseusRuntimeEngine = class {
3574
3706
  return {
3575
3707
  execute: async (context, request) => {
3576
3708
  const startedAt = Date.now();
3709
+ const operationId = digestRuntimeValue(`${command.commandId}:${request.requestId}`);
3710
+ const startedPresentation = codeToolRequestPresentation(request);
3577
3711
  await this.#event(
3578
3712
  command,
3579
- { type: "tool", phase: "started", tool: request.tool },
3713
+ {
3714
+ type: "tool",
3715
+ phase: "started",
3716
+ tool: request.tool,
3717
+ operationId,
3718
+ ...startedPresentation ? { presentation: startedPresentation } : {}
3719
+ },
3580
3720
  active.conversationRefs
3581
3721
  ).catch(() => void 0);
3582
3722
  const response2 = await broker.execute(context, request);
3723
+ const completedPresentation = codeToolResultPresentation(request, response2);
3583
3724
  await this.#event(command, {
3584
3725
  type: "tool",
3585
3726
  phase: "completed",
3586
3727
  tool: request.tool,
3587
3728
  ok: response2.ok,
3588
- durationMs: Date.now() - startedAt
3729
+ durationMs: Date.now() - startedAt,
3730
+ operationId,
3731
+ ...completedPresentation ? { presentation: completedPresentation } : {}
3589
3732
  }, active.conversationRefs).catch(() => void 0);
3590
3733
  return response2;
3591
3734
  }
@@ -3830,9 +3973,9 @@ var MEASURED_PREMIUM = Object.freeze({
3830
3973
  decomposePerSubGoal: 0.23
3831
3974
  });
3832
3975
  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);
3976
+ const text2 = feedback.trim();
3977
+ if (text2.length < 12) return false;
3978
+ 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
3979
  };
3837
3980
  function chooseStrategy(signals = {}) {
3838
3981
  const width = Math.max(1, signals.width ?? 3);