@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.
@@ -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";
@@ -1626,6 +1626,7 @@ async function runCodeAgentAttempt(options) {
1626
1626
  model: "brokered",
1627
1627
  surface,
1628
1628
  ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
1629
+ ...options.extraSkills ? { extraSkills: options.extraSkills } : {},
1629
1630
  ...options.maxSteps === void 0 ? {} : { maxSteps: options.maxSteps },
1630
1631
  ...options.budget ? { budget: options.budget } : {},
1631
1632
  ...options.signal ? { signal: options.signal } : {},
@@ -1657,6 +1658,18 @@ Finish with a concise, non-empty answer to the owner. Do not call tools or promi
1657
1658
  }
1658
1659
  }
1659
1660
 
1661
+ // src/code-runtime-session-skills.ts
1662
+ async function sessionSkillsFor(options, command) {
1663
+ try {
1664
+ return await options.sessionSkills?.(command) ?? [];
1665
+ } catch (cause) {
1666
+ options.onDiagnostic?.(
1667
+ `session skills unavailable, continuing with code tools only: ${cause instanceof Error ? cause.message : String(cause)}`
1668
+ );
1669
+ return [];
1670
+ }
1671
+ }
1672
+
1660
1673
  // src/code-runtime-inference.ts
1661
1674
  async function handleCodeRuntimeInference(input) {
1662
1675
  const { command, request, state } = input;
@@ -1838,16 +1851,16 @@ function createCodePolicyGate(options) {
1838
1851
  }
1839
1852
  };
1840
1853
  }
1841
- function directoryPrefixes(paths) {
1854
+ function directoryPrefixes(paths2) {
1842
1855
  const prefixes = /* @__PURE__ */ new Set(["."]);
1843
- for (const path of paths) {
1856
+ for (const path of paths2) {
1844
1857
  const parts = path.split("/");
1845
1858
  for (let index = 1; index < parts.length; index += 1) prefixes.add(parts.slice(0, index).join("/"));
1846
1859
  }
1847
1860
  return [...prefixes].sort();
1848
1861
  }
1849
- async function safePrefix(base, paths, prefix) {
1850
- const prefixes = directoryPrefixes(paths);
1862
+ async function safePrefix(base, paths2, prefix) {
1863
+ const prefixes = directoryPrefixes(paths2);
1851
1864
  const conversions = await conversionRegistry(
1852
1865
  [await registeredPolicy("code.prefix.v1", "code.prefixes.v1", prefixes)],
1853
1866
  { "code.prefixes.v1": prefixes }
@@ -1977,7 +1990,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
1977
1990
  files(root) {
1978
1991
  const existing = cache2.get(root);
1979
1992
  if (existing) return existing;
1980
- const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
1993
+ const pending = enumerate(root, limit).then((paths2) => Object.freeze(paths2));
1981
1994
  cache2.set(root, pending);
1982
1995
  void pending.catch(() => {
1983
1996
  if (cache2.get(root) === pending) cache2.delete(root);
@@ -1990,7 +2003,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
1990
2003
  };
1991
2004
  }
1992
2005
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
1993
- const paths = [];
2006
+ const paths2 = [];
1994
2007
  const walk = async (directory) => {
1995
2008
  for (const entry of await (0, import_promises7.readdir)(directory, { withFileTypes: true })) {
1996
2009
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
@@ -2004,26 +2017,26 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2004
2017
  } catch {
2005
2018
  continue;
2006
2019
  }
2007
- paths.push(path);
2008
- if (paths.length > limit) throw new TypeError("workspace file registry exceeds its bound");
2020
+ paths2.push(path);
2021
+ if (paths2.length > limit) throw new TypeError("workspace file registry exceeds its bound");
2009
2022
  }
2010
2023
  }
2011
2024
  };
2012
2025
  await walk((0, import_node_path8.resolve)(root));
2013
- return paths.sort();
2026
+ return paths2.sort();
2014
2027
  }
2015
- function listWorkspace(paths, options = {}) {
2028
+ function listWorkspace(paths2, options = {}) {
2016
2029
  const max = options.maxEntries ?? 1e3;
2017
2030
  const prefix = options.prefix?.replace(/\/+$/, "");
2018
- const scoped = prefix ? paths.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths];
2031
+ const scoped = prefix ? paths2.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths2];
2019
2032
  return scoped.slice(0, max);
2020
2033
  }
2021
- async function searchWorkspace(root, paths, options) {
2034
+ async function searchWorkspace(root, paths2, options) {
2022
2035
  options.signal?.throwIfAborted();
2023
2036
  if (!options.query) throw new TypeError("search query must be a non-empty string");
2024
2037
  const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
2025
2038
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
2026
- const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
2039
+ const scoped = listWorkspace(paths2, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths2.length });
2027
2040
  if (scoped.length === 0) return [];
2028
2041
  try {
2029
2042
  return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
@@ -2033,11 +2046,11 @@ async function searchWorkspace(root, paths, options) {
2033
2046
  }
2034
2047
  }
2035
2048
  var MAX_NATIVE_ARG_BYTES = 96 * 1024;
2036
- async function nativeSearch(root, paths, options) {
2049
+ async function nativeSearch(root, paths2, options) {
2037
2050
  const batches = [];
2038
2051
  let batch = [];
2039
2052
  let bytes = 0;
2040
- for (const path of paths) {
2053
+ for (const path of paths2) {
2041
2054
  const size = Buffer.byteLength(path) + 1;
2042
2055
  if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
2043
2056
  batches.push(batch);
@@ -2056,7 +2069,7 @@ async function nativeSearch(root, paths, options) {
2056
2069
  }
2057
2070
  return matches;
2058
2071
  }
2059
- function nativeSearchBatch(root, paths, options, remaining) {
2072
+ function nativeSearchBatch(root, paths2, options, remaining) {
2060
2073
  return new Promise((resolveMatches, reject) => {
2061
2074
  const args = [
2062
2075
  "--fixed-strings",
@@ -2069,7 +2082,7 @@ function nativeSearchBatch(root, paths, options, remaining) {
2069
2082
  options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
2070
2083
  "--",
2071
2084
  options.query,
2072
- ...paths
2085
+ ...paths2
2073
2086
  ];
2074
2087
  const child = (0, import_node_child_process5.spawn)("rg", args, {
2075
2088
  cwd: root,
@@ -2146,7 +2159,7 @@ var import_node_path9 = require("path");
2146
2159
  var import_graph = require("@odla-ai/graph");
2147
2160
  var import_code4 = require("@odla-ai/graph/code");
2148
2161
  var cache = /* @__PURE__ */ new Map();
2149
- function workspaceGraphs(workspaceDir, paths) {
2162
+ function workspaceGraphs(workspaceDir, paths2) {
2150
2163
  const existing = cache.get(workspaceDir);
2151
2164
  if (existing) return existing;
2152
2165
  const read2 = (path) => (0, import_promises8.readFile)((0, import_node_path9.join)(workspaceDir, path), "utf8");
@@ -2154,7 +2167,7 @@ function workspaceGraphs(workspaceDir, paths) {
2154
2167
  // No knownTables: a staged workspace may not carry migrations, and a filter
2155
2168
  // that silently drops every table is worse than an unfiltered one. Callers
2156
2169
  // with ground truth should build the graph themselves.
2157
- graph: await (0, import_code4.buildCodeGraph)({ paths, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2170
+ graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2158
2171
  }))();
2159
2172
  cache.set(workspaceDir, built);
2160
2173
  return built;
@@ -2216,11 +2229,11 @@ async function read(context, request, options, policy, registry) {
2216
2229
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
2217
2230
  throw new TypeError("requested line range exceeds its bound");
2218
2231
  }
2219
- const paths = await registry.files(context.workspaceDir);
2220
- if (!paths.includes(path)) {
2232
+ const paths2 = await registry.files(context.workspaceDir);
2233
+ if (!paths2.includes(path)) {
2221
2234
  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
2235
  }
2223
- const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
2236
+ const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
2224
2237
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2225
2238
  const target = resolveCodePath(context.workspaceDir, path);
2226
2239
  const info = await (0, import_promises9.stat)(target);
@@ -2242,16 +2255,16 @@ async function list(context, request, options, policy, registry) {
2242
2255
  const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
2243
2256
  const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
2244
2257
  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 } : {} }));
2258
+ const paths2 = await registry.files(context.workspaceDir);
2259
+ const allowed = await policy.list(policyContext(context, request, options, { paths: paths2, ...prefix ? { prefix } : {} }));
2247
2260
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2248
- const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
2261
+ const entries = listWorkspace(paths2, { ...prefix ? { prefix } : {}, maxEntries });
2249
2262
  if (!entries.length) {
2250
2263
  return response(request, true, prefix ? `No files under "${prefix}".` : "Workspace is empty.", { count: 0 });
2251
2264
  }
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.` : "";
2265
+ const truncated = entries.length < paths2.length && entries.length === maxEntries;
2266
+ const hint = !prefix && paths2.length > 500 ? `
2267
+ \u2026 ${paths2.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
2255
2268
  return response(
2256
2269
  request,
2257
2270
  true,
@@ -2269,10 +2282,10 @@ async function search(context, request, options, policy, registry) {
2269
2282
  const maxResults = optionalInteger(request.input.maxResults) ?? 100;
2270
2283
  if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
2271
2284
  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 } : {} }));
2285
+ const paths2 = await registry.files(context.workspaceDir);
2286
+ const allowed = await policy.search(policyContext(context, request, options, { paths: paths2, query, ...prefix ? { prefix } : {} }));
2274
2287
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2275
- const matches = await searchWorkspace(context.workspaceDir, paths, {
2288
+ const matches = await searchWorkspace(context.workspaceDir, paths2, {
2276
2289
  query,
2277
2290
  maxResults,
2278
2291
  caseSensitive,
@@ -2294,8 +2307,8 @@ async function graphQuery(context, request, options, policy, registry) {
2294
2307
  selector: query
2295
2308
  }));
2296
2309
  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);
2310
+ const paths2 = await registry.files(context.workspaceDir);
2311
+ const graphs = await workspaceGraphs(context.workspaceDir, paths2);
2299
2312
  if (request.tool === "sandbox.overview") {
2300
2313
  return response(request, true, renderOverview(graphs, query || void 0));
2301
2314
  }
@@ -2359,16 +2372,16 @@ function toolFailureMessage(reason) {
2359
2372
  async function patch(context, request, options, policy, registry) {
2360
2373
  exactKeys(request.input, ["patch"]);
2361
2374
  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}/`)))) {
2375
+ const paths2 = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2376
+ if (paths2.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
2364
2377
  throw new TypeError("patch targets a read-only reference source");
2365
2378
  }
2366
2379
  const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
2367
2380
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2368
- await applyCodePatch(context.workspaceDir, value, paths);
2381
+ await applyCodePatch(context.workspaceDir, value, paths2);
2369
2382
  registry.invalidate(context.workspaceDir);
2370
2383
  forgetWorkspaceGraphs(context.workspaceDir);
2371
- return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
2384
+ return response(request, true, `Applied patch to ${paths2.length} file(s).`, { paths: paths2 });
2372
2385
  }
2373
2386
  async function recipe(context, request, options, recipes, policy) {
2374
2387
  exactKeys(request.input, ["recipeId"]);
@@ -2729,12 +2742,129 @@ var import_node_crypto4 = require("crypto");
2729
2742
  async function appendCodeRuntimeEvent(control, command, event, refs) {
2730
2743
  const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
2731
2744
  refs.push(eventId);
2732
- const bounded = event.type === "message" ? { ...event, body: event.body.trim().slice(0, 2e4) || `${event.actor} event` } : event;
2745
+ const attributed = { ...event, interactionId: command.commandId };
2746
+ const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
2733
2747
  await control.appendSessionEvent(command.sessionId, eventId, bounded);
2734
2748
  }
2735
2749
  var digestRuntimeValue = (value) => `sha256:${(0, import_node_crypto4.createHash)("sha256").update(value).digest("hex")}`;
2736
2750
  var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
2737
2751
 
2752
+ // src/code-tool-presentation.ts
2753
+ var text = (value, maximum) => {
2754
+ if (typeof value !== "string") return void 0;
2755
+ const bounded = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
2756
+ return bounded ? bounded.slice(0, maximum) : void 0;
2757
+ };
2758
+ var integer2 = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : void 0;
2759
+ var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2760
+ var excerpt = (value, tail = false) => {
2761
+ if (typeof value !== "string") return void 0;
2762
+ const safe = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
2763
+ const source = (tail ? safe.slice(-1e4) : safe.slice(0, 1e4)).trim();
2764
+ if (!source) return void 0;
2765
+ const lines = source.split("\n").filter((line) => line.trim()).map((line) => line.slice(0, 240));
2766
+ const selected = tail ? lines.slice(-10) : lines.slice(0, 10);
2767
+ return text(selected.join("\n"), 2400);
2768
+ };
2769
+ var paths = (value) => {
2770
+ if (!Array.isArray(value)) return void 0;
2771
+ const items = value.flatMap((item) => {
2772
+ const path = text(item, 1024);
2773
+ return path ? [path] : [];
2774
+ }).slice(0, 12);
2775
+ return items.length ? items : void 0;
2776
+ };
2777
+ function patchStats(value) {
2778
+ if (typeof value !== "string") return {};
2779
+ let additions = 0;
2780
+ let deletions = 0;
2781
+ for (const line of value.slice(0, 262144).split("\n")) {
2782
+ if (line.startsWith("+++") || line.startsWith("---")) continue;
2783
+ if (line.startsWith("+")) additions += 1;
2784
+ else if (line.startsWith("-")) deletions += 1;
2785
+ }
2786
+ return { ...additions ? { additions } : {}, ...deletions ? { deletions } : {} };
2787
+ }
2788
+ function searchResults(value) {
2789
+ if (typeof value !== "string") return void 0;
2790
+ const results = value.split("\n").flatMap((line) => {
2791
+ const match = /^([^:\n]{1,1024}):(\d+):\s?(.*)$/.exec(line);
2792
+ if (!match) return [];
2793
+ const lineNumber = Number(match[2]);
2794
+ const itemText = text(match[3], 240);
2795
+ if (!Number.isSafeInteger(lineNumber) || lineNumber < 1) return [];
2796
+ return [{ path: match[1], line: lineNumber, ...itemText ? { text: itemText } : {} }];
2797
+ }).slice(0, 5);
2798
+ return results.length ? results : void 0;
2799
+ }
2800
+ function codeToolRequestPresentation(request) {
2801
+ const input = request.input;
2802
+ if (request.tool === "sandbox.read") {
2803
+ const path = text(input.path, 1024);
2804
+ if (!path) return void 0;
2805
+ const startLine = integer2(input.startLine);
2806
+ const endLine = integer2(input.endLine);
2807
+ return { kind: "read", path, ...startLine ? { startLine } : {}, ...endLine ? { endLine } : {} };
2808
+ }
2809
+ if (request.tool === "sandbox.list") {
2810
+ const scope = text(input.prefix, 1024);
2811
+ return { kind: "list", ...scope ? { scope } : {} };
2812
+ }
2813
+ if (request.tool === "sandbox.search" || request.tool === "sandbox.overview" || request.tool === "sandbox.where_is" || request.tool === "sandbox.who_imports" || request.tool === "sandbox.who_touches") {
2814
+ const query = text(input.query, 512);
2815
+ const scope = request.tool === "sandbox.search" ? text(input.prefix, 1024) : void 0;
2816
+ if (request.tool === "sandbox.search" && !query) return void 0;
2817
+ return { kind: "query", ...query ? { query } : {}, ...scope ? { scope } : {} };
2818
+ }
2819
+ if (request.tool === "sandbox.apply_patch") {
2820
+ return { kind: "patch", ...patchStats(input.patch) };
2821
+ }
2822
+ const recipeId = text(input.recipeId, 120);
2823
+ return recipeId ? { kind: "recipe", recipeId } : void 0;
2824
+ }
2825
+ function codeToolResultPresentation(request, response2) {
2826
+ const started = codeToolRequestPresentation(request);
2827
+ if (!started || !response2.ok) return started;
2828
+ const details = record3(response2.details);
2829
+ if (started.kind === "read") {
2830
+ return {
2831
+ ...started,
2832
+ ...integer2(details?.startLine) ? { startLine: integer2(details?.startLine) } : {},
2833
+ ...integer2(details?.endLine) ? { endLine: integer2(details?.endLine) } : {},
2834
+ ...excerpt(response2.content) ? { excerpt: excerpt(response2.content) } : {}
2835
+ };
2836
+ }
2837
+ if (started.kind === "list") {
2838
+ 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);
2839
+ return {
2840
+ ...started,
2841
+ ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
2842
+ ...listed.length ? { paths: listed } : {}
2843
+ };
2844
+ }
2845
+ if (started.kind === "query") {
2846
+ const results = request.tool === "sandbox.search" ? searchResults(response2.content) : void 0;
2847
+ const resultExcerpt = request.tool === "sandbox.search" ? void 0 : excerpt(response2.content);
2848
+ return {
2849
+ ...started,
2850
+ ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
2851
+ ...results ? { results } : {},
2852
+ ...resultExcerpt ? { excerpt: resultExcerpt } : {}
2853
+ };
2854
+ }
2855
+ if (started.kind === "patch") {
2856
+ return { ...started, ...paths(details?.paths) ? { paths: paths(details?.paths) } : {} };
2857
+ }
2858
+ const output = response2.content.replace(/^Recipe [^\n]*\.?\s*/u, "");
2859
+ return {
2860
+ ...started,
2861
+ ...integer2(details?.exitCode) !== void 0 ? { exitCode: integer2(details?.exitCode) } : {},
2862
+ ...typeof details?.timedOut === "boolean" ? { timedOut: details.timedOut } : {},
2863
+ ...typeof details?.outputLimitExceeded === "boolean" ? { outputLimitExceeded: details.outputLimitExceeded } : {},
2864
+ ...excerpt(output, true) ? { excerpt: excerpt(output, true) } : {}
2865
+ };
2866
+ }
2867
+
2738
2868
  // src/code-runtime-engine.ts
2739
2869
  var TheseusRuntimeEngine = class {
2740
2870
  constructor(options) {
@@ -2929,6 +3059,7 @@ var TheseusRuntimeEngine = class {
2929
3059
  event: (event) => this.#event(command, event, active.conversationRefs)
2930
3060
  });
2931
3061
  await this.#event(command, { type: "status", status: "running" }, active.conversationRefs);
3062
+ const extraSkills = await sessionSkillsFor(this.options, command);
2932
3063
  const result = await this.#attempt({
2933
3064
  inference,
2934
3065
  broker,
@@ -2936,7 +3067,8 @@ var TheseusRuntimeEngine = class {
2936
3067
  workspaceDir: active.workspace.workspaceDir,
2937
3068
  prompt: metadata.prompt,
2938
3069
  signal: active.abort.signal,
2939
- recipeIds: this.options.recipes.map((recipe2) => recipe2.id)
3070
+ recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
3071
+ ...extraSkills.length ? { extraSkills } : {}
2940
3072
  });
2941
3073
  const closing = result.finalText.trim();
2942
3074
  const completed = result.status === "completed" && Boolean(closing);
@@ -2969,18 +3101,29 @@ var TheseusRuntimeEngine = class {
2969
3101
  return {
2970
3102
  execute: async (context, request) => {
2971
3103
  const startedAt = Date.now();
3104
+ const operationId = digestRuntimeValue(`${command.commandId}:${request.requestId}`);
3105
+ const startedPresentation = codeToolRequestPresentation(request);
2972
3106
  await this.#event(
2973
3107
  command,
2974
- { type: "tool", phase: "started", tool: request.tool },
3108
+ {
3109
+ type: "tool",
3110
+ phase: "started",
3111
+ tool: request.tool,
3112
+ operationId,
3113
+ ...startedPresentation ? { presentation: startedPresentation } : {}
3114
+ },
2975
3115
  active.conversationRefs
2976
3116
  ).catch(() => void 0);
2977
3117
  const response2 = await broker.execute(context, request);
3118
+ const completedPresentation = codeToolResultPresentation(request, response2);
2978
3119
  await this.#event(command, {
2979
3120
  type: "tool",
2980
3121
  phase: "completed",
2981
3122
  tool: request.tool,
2982
3123
  ok: response2.ok,
2983
- durationMs: Date.now() - startedAt
3124
+ durationMs: Date.now() - startedAt,
3125
+ operationId,
3126
+ ...completedPresentation ? { presentation: completedPresentation } : {}
2984
3127
  }, active.conversationRefs).catch(() => void 0);
2985
3128
  return response2;
2986
3129
  }