zidane 3.0.2 → 3.1.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.
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-TPXPVEH6.js";
9
9
  import {
10
10
  createProcessContext
11
- } from "./chunk-2EQT4EHD.js";
11
+ } from "./chunk-IUBBVF53.js";
12
12
  import {
13
13
  connectMcpServers
14
14
  } from "./chunk-R74LQKAM.js";
@@ -36,6 +36,175 @@ function countExactMatches(haystack, needle) {
36
36
  }
37
37
  return count;
38
38
  }
39
+ var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
40
+ var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
41
+ var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
42
+ var RIGHT_DOUBLE_CURLY_QUOTE = "\u201D";
43
+ function normalizeQuotes(str) {
44
+ return str.replaceAll(LEFT_SINGLE_CURLY_QUOTE, "'").replaceAll(RIGHT_SINGLE_CURLY_QUOTE, "'").replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '"').replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '"');
45
+ }
46
+ var DESANITIZATIONS = [
47
+ ["<fnr>", "<function_results>"],
48
+ ["<n>", "<name>"],
49
+ ["</n>", "</name>"],
50
+ ["<o>", "<output>"],
51
+ ["</o>", "</output>"],
52
+ ["<e>", "<error>"],
53
+ ["</e>", "</error>"],
54
+ ["<s>", "<system>"],
55
+ ["</s>", "</system>"],
56
+ ["<r>", "<result>"],
57
+ ["</r>", "</result>"],
58
+ ["< META_START >", "<META_START>"],
59
+ ["< META_END >", "<META_END>"],
60
+ ["< EOT >", "<EOT>"],
61
+ ["< META >", "<META>"],
62
+ ["< SOS >", "<SOS>"],
63
+ ["\n\nH:", "\n\nHuman:"],
64
+ ["\n\nA:", "\n\nAssistant:"]
65
+ ];
66
+ function desanitize(s) {
67
+ let out = s;
68
+ for (const [from, to] of DESANITIZATIONS)
69
+ out = out.replaceAll(from, to);
70
+ return out;
71
+ }
72
+ function resolveOldString(haystack, needle) {
73
+ const exact = countExactMatches(haystack, needle);
74
+ if (exact > 0)
75
+ return { actual: needle, occurrences: exact, via: "exact" };
76
+ const normNeedle = normalizeQuotes(needle);
77
+ const normFile = normalizeQuotes(haystack);
78
+ if (normNeedle !== needle || normFile !== haystack) {
79
+ const idx = normFile.indexOf(normNeedle);
80
+ if (idx !== -1) {
81
+ const actual = haystack.slice(idx, idx + normNeedle.length);
82
+ let occ = 0;
83
+ let cursor = 0;
84
+ while (true) {
85
+ const next = normFile.indexOf(normNeedle, cursor);
86
+ if (next === -1)
87
+ break;
88
+ occ++;
89
+ cursor = next + normNeedle.length;
90
+ }
91
+ return { actual, occurrences: occ, via: "quotes" };
92
+ }
93
+ }
94
+ const desan = desanitize(needle);
95
+ if (desan !== needle) {
96
+ const desanCount = countExactMatches(haystack, desan);
97
+ if (desanCount > 0)
98
+ return { actual: desan, occurrences: desanCount, via: "desanitize" };
99
+ }
100
+ const combo = desanitize(normNeedle);
101
+ if (combo !== needle) {
102
+ const idx = normFile.indexOf(combo);
103
+ if (idx !== -1) {
104
+ const actual = haystack.slice(idx, idx + combo.length);
105
+ let occ = 0;
106
+ let cursor = 0;
107
+ while (true) {
108
+ const next = normFile.indexOf(combo, cursor);
109
+ if (next === -1)
110
+ break;
111
+ occ++;
112
+ cursor = next + combo.length;
113
+ }
114
+ return { actual, occurrences: occ, via: "quotes+desanitize" };
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+ function preserveQuoteStyle(actual, replacement) {
120
+ const hasDouble = actual.includes(LEFT_DOUBLE_CURLY_QUOTE) || actual.includes(RIGHT_DOUBLE_CURLY_QUOTE);
121
+ const hasSingle = actual.includes(LEFT_SINGLE_CURLY_QUOTE) || actual.includes(RIGHT_SINGLE_CURLY_QUOTE);
122
+ if (!hasDouble && !hasSingle)
123
+ return replacement;
124
+ let out = replacement;
125
+ if (hasDouble)
126
+ out = applyCurly(out, '"', LEFT_DOUBLE_CURLY_QUOTE, RIGHT_DOUBLE_CURLY_QUOTE, false);
127
+ if (hasSingle)
128
+ out = applyCurly(out, "'", LEFT_SINGLE_CURLY_QUOTE, RIGHT_SINGLE_CURLY_QUOTE, true);
129
+ return out;
130
+ }
131
+ function applyCurly(s, straight, left, right, contractionAware) {
132
+ const chars = [...s];
133
+ const result = [];
134
+ for (let i = 0; i < chars.length; i++) {
135
+ if (chars[i] !== straight) {
136
+ result.push(chars[i]);
137
+ continue;
138
+ }
139
+ if (contractionAware) {
140
+ const prev = i > 0 ? chars[i - 1] : "";
141
+ const next = i < chars.length - 1 ? chars[i + 1] : "";
142
+ if (/\p{L}/u.test(prev) && /\p{L}/u.test(next)) {
143
+ result.push(right);
144
+ continue;
145
+ }
146
+ }
147
+ result.push(isOpeningContext(chars, i) ? left : right);
148
+ }
149
+ return result.join("");
150
+ }
151
+ function isOpeningContext(chars, i) {
152
+ if (i === 0)
153
+ return true;
154
+ const prev = chars[i - 1];
155
+ return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "\u2014" || prev === "\u2013";
156
+ }
157
+
158
+ // src/tools/path-suggest.ts
159
+ async function findSimilarFile(execution, handle, path) {
160
+ const slash = path.lastIndexOf("/");
161
+ const dir = slash === -1 ? "." : path.slice(0, slash) || "/";
162
+ const target = slash === -1 ? path : path.slice(slash + 1);
163
+ const dot = target.lastIndexOf(".");
164
+ const targetBase = dot === -1 ? target : target.slice(0, dot);
165
+ if (targetBase.length === 0)
166
+ return null;
167
+ let entries;
168
+ try {
169
+ entries = await execution.listFiles(handle, dir);
170
+ } catch {
171
+ return null;
172
+ }
173
+ for (const entry of entries) {
174
+ if (entry === target)
175
+ continue;
176
+ const entryDot = entry.lastIndexOf(".");
177
+ const entryBase = entryDot === -1 ? entry : entry.slice(0, entryDot);
178
+ if (entryBase === targetBase)
179
+ return entry;
180
+ }
181
+ return null;
182
+ }
183
+ async function suggestionFor(execution, handle, path) {
184
+ const sibling = await findSimilarFile(execution, handle, path);
185
+ return sibling ? ` Did you mean ${sibling}?` : "";
186
+ }
187
+
188
+ // src/tools/read-state.ts
189
+ var STATE = /* @__PURE__ */ new WeakMap();
190
+ function getReadState(session) {
191
+ if (!session)
192
+ return void 0;
193
+ let map = STATE.get(session);
194
+ if (!map) {
195
+ map = /* @__PURE__ */ new Map();
196
+ STATE.set(session, map);
197
+ }
198
+ return map;
199
+ }
200
+ function hashContent(text) {
201
+ let h = 2166136261;
202
+ for (let i = 0; i < text.length; i++) {
203
+ h ^= text.charCodeAt(i);
204
+ h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
205
+ }
206
+ return h.toString(16).padStart(8, "0");
207
+ }
39
208
 
40
209
  // src/tools/edit.ts
41
210
  var edit = {
@@ -66,19 +235,41 @@ var edit = {
66
235
  try {
67
236
  original = await ctx.execution.readFile(ctx.handle, target);
68
237
  } catch {
69
- return `Edit error: file not found: ${target}`;
70
- }
71
- const occurrences = countExactMatches(original, find);
72
- if (occurrences === 0) {
238
+ const hint = await suggestionFor(ctx.execution, ctx.handle, target);
239
+ return `Edit error: file not found: ${target}.${hint}`;
240
+ }
241
+ if (ctx.behavior?.requireReadBeforeEdit && ctx.session) {
242
+ const readState = getReadState(ctx.session);
243
+ const absKey = `${ctx.handle.cwd}::${target}`;
244
+ const prior = readState?.get(absKey);
245
+ if (!prior)
246
+ return `Edit error: ${target} has not been read in this session. Call read_file first so the edit applies against the current contents.`;
247
+ if (prior.contentHash !== hashContent(original))
248
+ return `Edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
249
+ }
250
+ const match = resolveOldString(original, find);
251
+ if (!match) {
73
252
  const preview = nearestMatchPreview(original, find);
74
253
  return preview ? `Edit error: old_string not found in ${target}. Closest match in the file: ${preview}` : `Edit error: old_string not found in ${target}.`;
75
254
  }
255
+ const { actual, occurrences, via } = match;
76
256
  if (occurrences > 1 && !replaceAll)
77
257
  return `Edit error: old_string appears ${occurrences} times in ${target}. Pass replace_all=true or expand old_string for uniqueness.`;
78
- const updated = replaceAll ? original.split(find).join(replacement) : original.replace(find, replacement);
258
+ let styledReplacement = via === "desanitize" || via === "quotes+desanitize" ? desanitize(replacement) : replacement;
259
+ if (via === "quotes" || via === "quotes+desanitize")
260
+ styledReplacement = preserveQuoteStyle(actual, styledReplacement);
261
+ const updated = replaceAll ? original.split(actual).join(styledReplacement) : original.replace(actual, styledReplacement);
79
262
  if (updated === original)
80
263
  return `Edit error: replacement produced no change in ${target}.`;
81
264
  await ctx.execution.writeFile(ctx.handle, target, updated);
265
+ if (ctx.session) {
266
+ const readState = getReadState(ctx.session);
267
+ const absKey = `${ctx.handle.cwd}::${target}`;
268
+ const prior = readState?.get(absKey);
269
+ if (readState && prior) {
270
+ readState.set(absKey, { ...prior, contentHash: hashContent(updated), mtimeMs: Date.now() });
271
+ }
272
+ }
82
273
  return `Edited ${target}: replaced ${occurrences} occurrence${occurrences === 1 ? "" : "s"}.`;
83
274
  }
84
275
  };
@@ -441,7 +632,17 @@ var multiEdit = {
441
632
  try {
442
633
  current = await ctx.execution.readFile(ctx.handle, target);
443
634
  } catch {
444
- return `multi_edit error: file not found: ${target}`;
635
+ const hint = await suggestionFor(ctx.execution, ctx.handle, target);
636
+ return `multi_edit error: file not found: ${target}.${hint}`;
637
+ }
638
+ if (ctx.behavior?.requireReadBeforeEdit && ctx.session) {
639
+ const readState = getReadState(ctx.session);
640
+ const absKey = `${ctx.handle.cwd}::${target}`;
641
+ const prior = readState?.get(absKey);
642
+ if (!prior)
643
+ return `multi_edit error: ${target} has not been read in this session. Call read_file first so the edits apply against the current contents.`;
644
+ if (prior.contentHash !== hashContent(current))
645
+ return `multi_edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
445
646
  }
446
647
  let applied = 0;
447
648
  for (let i = 0; i < steps.length; i++) {
@@ -455,24 +656,77 @@ var multiEdit = {
455
656
  return `multi_edit error: edit #${i + 1} has empty old_string. Use write_file to fully replace a file.`;
456
657
  if (find === replacement)
457
658
  return `multi_edit error: edit #${i + 1} old_string and new_string are identical.`;
458
- const occurrences = countExactMatches(current, find);
459
- if (occurrences === 0)
659
+ const match = resolveOldString(current, find);
660
+ if (!match)
460
661
  return `multi_edit error: edit #${i + 1} old_string not found in ${target}.`;
662
+ const { actual, occurrences, via } = match;
461
663
  if (occurrences > 1 && !replaceAll)
462
664
  return `multi_edit error: edit #${i + 1} old_string appears ${occurrences} times. Pass replace_all=true on this edit or expand old_string for uniqueness.`;
463
- current = replaceAll ? current.split(find).join(replacement) : current.replace(find, replacement);
665
+ let styledReplacement = via === "desanitize" || via === "quotes+desanitize" ? desanitize(replacement) : replacement;
666
+ if (via === "quotes" || via === "quotes+desanitize")
667
+ styledReplacement = preserveQuoteStyle(actual, styledReplacement);
668
+ current = replaceAll ? current.split(actual).join(styledReplacement) : current.replace(actual, styledReplacement);
464
669
  applied += occurrences;
465
670
  }
466
671
  await ctx.execution.writeFile(ctx.handle, target, current);
672
+ if (ctx.session) {
673
+ const readState = getReadState(ctx.session);
674
+ const absKey = `${ctx.handle.cwd}::${target}`;
675
+ const prior = readState?.get(absKey);
676
+ if (readState && prior) {
677
+ readState.set(absKey, { ...prior, contentHash: hashContent(current), mtimeMs: Date.now() });
678
+ }
679
+ }
467
680
  return `Edited ${target}: applied ${steps.length} edit${steps.length === 1 ? "" : "s"} (${applied} replacement${applied === 1 ? "" : "s"}).`;
468
681
  }
469
682
  };
470
683
 
471
684
  // src/tools/read-file.ts
685
+ import { Buffer as Buffer2 } from "buffer";
686
+
687
+ // src/tools/binary-read.ts
472
688
  import { Buffer } from "buffer";
689
+ function imageMediaTypeFor(path) {
690
+ const dot = path.lastIndexOf(".");
691
+ if (dot === -1)
692
+ return void 0;
693
+ const ext = path.slice(dot + 1).toLowerCase();
694
+ switch (ext) {
695
+ case "png":
696
+ return "image/png";
697
+ case "jpg":
698
+ case "jpeg":
699
+ return "image/jpeg";
700
+ case "gif":
701
+ return "image/gif";
702
+ case "webp":
703
+ return "image/webp";
704
+ default:
705
+ return void 0;
706
+ }
707
+ }
708
+ async function readFileAsBase64(execution, handle, path) {
709
+ if (execution.readFileBinary) {
710
+ const bytes = await execution.readFileBinary(handle, path);
711
+ const b642 = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
712
+ return { base64: b642, byteLength: bytes.byteLength };
713
+ }
714
+ const cmd = `base64 < ${shellQuote2(path)}`;
715
+ const result = await execution.exec(handle, cmd);
716
+ if (result.exitCode !== 0)
717
+ throw new Error(`base64 read failed: ${result.stderr || `exit ${result.exitCode}`}`);
718
+ const b64 = result.stdout.replace(/\s+/g, "");
719
+ return { base64: b64, byteLength: Math.floor(b64.length * 3 / 4) };
720
+ }
721
+ function shellQuote2(s) {
722
+ return `'${s.replace(/'/g, "'\\''")}'`;
723
+ }
724
+
725
+ // src/tools/read-file.ts
473
726
  var DEFAULT_LINE_LIMIT = 2e3;
474
727
  var DEFAULT_BYTE_CAP = 65536;
475
728
  var BINARY_PROBE_BYTES = 8e3;
729
+ var DEFAULT_IMAGE_BYTE_CAP = 5 * 1024 * 1024;
476
730
  var readFile = {
477
731
  spec: {
478
732
  name: "read_file",
@@ -489,19 +743,52 @@ var readFile = {
489
743
  }
490
744
  },
491
745
  async execute({ path, offset, limit, maxBytes }, ctx) {
746
+ const imgMedia = imageMediaTypeFor(path);
747
+ if (imgMedia) {
748
+ const sizeCap = maxBytes !== void 0 ? normalizeInteger(maxBytes, DEFAULT_IMAGE_BYTE_CAP) : DEFAULT_IMAGE_BYTE_CAP;
749
+ try {
750
+ const { base64, byteLength } = await readFileAsBase64(ctx.execution, ctx.handle, path);
751
+ if (sizeCap > 0 && byteLength > sizeCap) {
752
+ return `[image too large to inline: ${path}, ${byteLength} bytes (cap ${sizeCap}). Raise maxBytes, or use shell to inspect.]`;
753
+ }
754
+ const content = [
755
+ { type: "text", text: `Image: ${path} (${byteLength} bytes, ${imgMedia})` },
756
+ { type: "image", mediaType: imgMedia, data: base64 }
757
+ ];
758
+ return content;
759
+ } catch (err) {
760
+ const msg = err instanceof Error ? err.message : String(err);
761
+ const hint = await suggestionFor(ctx.execution, ctx.handle, path);
762
+ return `Image read failed: ${path} \u2014 ${msg}.${hint}`;
763
+ }
764
+ }
492
765
  let raw;
493
766
  try {
494
767
  raw = await ctx.execution.readFile(ctx.handle, path);
495
768
  } catch {
496
- return `File not found: ${path}`;
769
+ const hint = await suggestionFor(ctx.execution, ctx.handle, path);
770
+ return `File not found: ${path}.${hint}`;
771
+ }
772
+ const totalBytes = Buffer2.byteLength(raw);
773
+ const dedupEnabled = ctx.behavior?.dedupReads !== false;
774
+ const readState = dedupEnabled ? getReadState(ctx.session) : void 0;
775
+ const absKey = `${ctx.handle.cwd}::${path}`;
776
+ const offsetForKey = normalizeInteger(offset, 1);
777
+ const limitForKey = normalizeInteger(limit, DEFAULT_LINE_LIMIT);
778
+ const maxBytesForKey = normalizeInteger(maxBytes, DEFAULT_BYTE_CAP);
779
+ const currentHash = readState ? hashContent(raw) : "";
780
+ if (readState) {
781
+ const prior = readState.get(absKey);
782
+ if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey) {
783
+ return `File ${path} unchanged since the previous read in this session \u2014 the prior result is still current.`;
784
+ }
497
785
  }
498
- const totalBytes = Buffer.byteLength(raw);
499
786
  if (looksBinary(raw)) {
500
787
  return `[binary file: ${path}, ${totalBytes} bytes; use shell with hexdump | xxd | od to inspect]`;
501
788
  }
502
- const offsetN = normalizeInteger(offset, 1);
503
- const limitN = normalizeInteger(limit, DEFAULT_LINE_LIMIT);
504
- const maxBytesN = normalizeInteger(maxBytes, DEFAULT_BYTE_CAP);
789
+ const offsetN = offsetForKey;
790
+ const limitN = limitForKey;
791
+ const maxBytesN = maxBytesForKey;
505
792
  const lines = raw.split("\n");
506
793
  const totalLines = lines.length;
507
794
  const startIdx = Math.max(0, offsetN - 1);
@@ -512,7 +799,7 @@ var readFile = {
512
799
  if (maxBytesN > 0) {
513
800
  const truncatedSlice = [];
514
801
  for (const line of slice) {
515
- const lineBytes = Buffer.byteLength(line) + 1;
802
+ const lineBytes = Buffer2.byteLength(line) + 1;
516
803
  if (bytesUsed + lineBytes > maxBytesN && truncatedSlice.length > 0) {
517
804
  bytesCut = true;
518
805
  break;
@@ -529,14 +816,14 @@ var readFile = {
529
816
  }
530
817
  let midLineCut = false;
531
818
  if (maxBytesN > 0 && slice.length > 0) {
532
- const bodyBytes = Buffer.byteLength(slice.join("\n"));
819
+ const bodyBytes = Buffer2.byteLength(slice.join("\n"));
533
820
  if (bodyBytes > maxBytesN) {
534
821
  const lastIdx = slice.length - 1;
535
822
  const lastLine = slice[lastIdx];
536
- const otherBytes = lastIdx > 0 ? Buffer.byteLength(slice.slice(0, lastIdx).join("\n")) + 1 : 0;
823
+ const otherBytes = lastIdx > 0 ? Buffer2.byteLength(slice.slice(0, lastIdx).join("\n")) + 1 : 0;
537
824
  const budgetForLast = Math.max(0, maxBytesN - otherBytes);
538
825
  let cut = Math.min(lastLine.length, budgetForLast);
539
- while (cut > 0 && Buffer.byteLength(lastLine.slice(0, cut)) > budgetForLast)
826
+ while (cut > 0 && Buffer2.byteLength(lastLine.slice(0, cut)) > budgetForLast)
540
827
  cut--;
541
828
  slice[lastIdx] = lastLine.slice(0, cut);
542
829
  midLineCut = true;
@@ -546,6 +833,15 @@ var readFile = {
546
833
  const body = slice.join("\n");
547
834
  const linesReturned = slice.length;
548
835
  const lastLineRead = startIdx + linesReturned;
836
+ if (readState) {
837
+ readState.set(absKey, {
838
+ contentHash: currentHash,
839
+ offset: offsetN,
840
+ limit: limitN,
841
+ maxBytes: maxBytesN,
842
+ mtimeMs: Date.now()
843
+ });
844
+ }
549
845
  const linesTruncated = endIdx < totalLines || bytesCut;
550
846
  if (!linesTruncated && offsetN === 1)
551
847
  return body;
@@ -589,7 +885,38 @@ function looksBinary(text) {
589
885
  }
590
886
 
591
887
  // src/tools/shell.ts
592
- import { Buffer as Buffer2 } from "buffer";
888
+ import { Buffer as Buffer3 } from "buffer";
889
+
890
+ // src/tools/shell-semantics.ts
891
+ var DEFAULT_SEMANTIC = (exitCode) => ({
892
+ isError: exitCode !== 0,
893
+ message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : void 0
894
+ });
895
+ var COMMAND_SEMANTICS = /* @__PURE__ */ new Map([
896
+ // grep / ripgrep: 0 = matches, 1 = no matches, ≥2 = error.
897
+ ["grep", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "No matches found" : void 0 })],
898
+ ["rg", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "No matches found" : void 0 })],
899
+ // diff: 0 = identical, 1 = differ, ≥2 = error.
900
+ ["diff", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Files differ" : void 0 })],
901
+ // find: 0 = ok, 1 = some dirs inaccessible (warning), ≥2 = error.
902
+ ["find", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Some directories were inaccessible" : void 0 })],
903
+ // test / [: 0 = condition true, 1 = condition false, ≥2 = error.
904
+ ["test", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Condition is false" : void 0 })],
905
+ ["[", (exit) => ({ isError: exit >= 2, message: exit === 1 ? "Condition is false" : void 0 })]
906
+ ]);
907
+ function interpretShellResult(command, exitCode) {
908
+ const base = extractTrailingCommand(command);
909
+ const semantic = COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC;
910
+ return semantic(exitCode);
911
+ }
912
+ function extractTrailingCommand(command) {
913
+ const segments = command.split(/\|\||&&|[;|\n]/);
914
+ const last = segments[segments.length - 1]?.trim() ?? command;
915
+ const tokens = last.split(/\s+/).filter((t) => !/^[A-Z_]\w*=/i.test(t));
916
+ return tokens[0] ?? "";
917
+ }
918
+
919
+ // src/tools/shell.ts
593
920
  var DEFAULT_MAX_OUTPUT_BYTES = 8192;
594
921
  var shell = {
595
922
  spec: {
@@ -609,10 +936,19 @@ var shell = {
609
936
  const execOpts = {};
610
937
  if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0)
611
938
  execOpts.timeout = Math.max(1, Math.ceil(timeout / 1e3));
612
- const result = await ctx.execution.exec(ctx.handle, command, execOpts);
939
+ const cmd = command;
940
+ const result = await ctx.execution.exec(ctx.handle, cmd, execOpts);
613
941
  const cap = normalizeCap(maxOutputBytes);
942
+ const semantic = interpretShellResult(cmd, result.exitCode);
614
943
  if (result.exitCode === 0)
615
944
  return truncateTail(result.stdout || "(no output)", cap);
945
+ if (!semantic.isError) {
946
+ const body = (result.stdout || result.stderr || "").trim();
947
+ const tail = truncateTail(body, cap);
948
+ const footer = semantic.message ? `
949
+ (${semantic.message})` : "";
950
+ return tail.length > 0 ? `${tail}${footer}` : semantic.message ?? "(no output)";
951
+ }
616
952
  const combined = `${result.stdout}
617
953
  ${result.stderr}`.trim();
618
954
  return `Exit code ${result.exitCode}
@@ -629,21 +965,21 @@ function normalizeCap(value) {
629
965
  function truncateTail(text, cap) {
630
966
  if (cap === 0)
631
967
  return text;
632
- const totalBytes = Buffer2.byteLength(text);
968
+ const totalBytes = Buffer3.byteLength(text);
633
969
  if (totalBytes <= cap)
634
970
  return text;
635
971
  let bytes = 0;
636
972
  let charIdx = text.length;
637
973
  while (charIdx > 0) {
638
974
  const ch = text[charIdx - 1];
639
- const chBytes = Buffer2.byteLength(ch);
975
+ const chBytes = Buffer3.byteLength(ch);
640
976
  if (bytes + chBytes > cap)
641
977
  break;
642
978
  bytes += chBytes;
643
979
  charIdx--;
644
980
  }
645
981
  const tail = text.slice(charIdx);
646
- const droppedBytes = totalBytes - Buffer2.byteLength(tail);
982
+ const droppedBytes = totalBytes - Buffer3.byteLength(tail);
647
983
  return `\u2026(${droppedBytes} bytes truncated from head)\u2026
648
984
  ${tail}`;
649
985
  }
@@ -1101,6 +1437,43 @@ var IMAGE_OMITTED_MARKER = "[image omitted \u2014 model does not support vision]
1101
1437
  function turnsToMessages(turns) {
1102
1438
  return turns.filter((t) => t.role !== "system").map((t) => ({ role: t.role, content: t.content }));
1103
1439
  }
1440
+ var COMPACTION_STUB = "[\u2026elided by client-side tail compaction; ask the user or re-run the tool to retrieve.]";
1441
+ function applyTailCompaction(messages, threshold, keepTurns) {
1442
+ if (messages.length === 0)
1443
+ return messages;
1444
+ let totalBytes = 0;
1445
+ for (const msg of messages) {
1446
+ for (const block of msg.content) {
1447
+ if (block.type === "tool_result")
1448
+ totalBytes += toolOutputByteLength(block.output);
1449
+ }
1450
+ }
1451
+ if (totalBytes <= threshold)
1452
+ return messages;
1453
+ const keep = Math.max(0, keepTurns);
1454
+ const cutoff = messages.length - keep;
1455
+ if (cutoff <= 0)
1456
+ return messages;
1457
+ let changed = false;
1458
+ const out = messages.slice();
1459
+ for (let i = 0; i < cutoff; i++) {
1460
+ const msg = out[i];
1461
+ let msgChanged = false;
1462
+ const newContent = msg.content.map((block) => {
1463
+ if (block.type !== "tool_result")
1464
+ return block;
1465
+ const existingBytes = toolOutputByteLength(block.output);
1466
+ if (existingBytes <= COMPACTION_STUB.length)
1467
+ return block;
1468
+ msgChanged = true;
1469
+ changed = true;
1470
+ return { ...block, output: COMPACTION_STUB };
1471
+ });
1472
+ if (msgChanged)
1473
+ out[i] = { ...msg, content: newContent };
1474
+ }
1475
+ return changed ? out : messages;
1476
+ }
1104
1477
  function sanitizeStoredToolResults(provider, messages) {
1105
1478
  if (provider.meta.capabilities?.vision !== false)
1106
1479
  return messages;
@@ -1210,7 +1583,12 @@ async function executeTurn(ctx, turn) {
1210
1583
  const turnId = await ctx.generateTurnId();
1211
1584
  const canonicalMessages = turnsToMessages(ctx.turns);
1212
1585
  const wireMessages = rewriteMessagesToWire(canonicalMessages, ctx.aliasMaps);
1213
- const sanitizedMessages = sanitizeStoredToolResults(ctx.provider, wireMessages);
1586
+ let sanitizedMessages = sanitizeStoredToolResults(ctx.provider, wireMessages);
1587
+ if (ctx.compactStrategy === "tail") {
1588
+ const threshold = typeof ctx.compactThreshold === "number" && ctx.compactThreshold > 0 ? ctx.compactThreshold : 131072;
1589
+ const keep = typeof ctx.compactKeepTurns === "number" && ctx.compactKeepTurns >= 0 ? ctx.compactKeepTurns : 4;
1590
+ sanitizedMessages = applyTailCompaction(sanitizedMessages, threshold, keep);
1591
+ }
1214
1592
  const streamOptions = {
1215
1593
  model: ctx.model,
1216
1594
  system: ctx.system,
@@ -1337,6 +1715,17 @@ async function executeTurn(ctx, turn) {
1337
1715
  }
1338
1716
  return { ended: true, turnId, usage: result.usage };
1339
1717
  }
1718
+ if (canonicalToolCalls.length === 0 && result.usage.finishReason === "pause") {
1719
+ const continueMsg = ctx.provider.userMessage("Please continue.");
1720
+ ctx.turns.push({
1721
+ id: await ctx.generateTurnId(),
1722
+ runId: ctx.runId,
1723
+ role: continueMsg.role,
1724
+ content: continueMsg.content,
1725
+ createdAt: Date.now()
1726
+ });
1727
+ return { ended: false, turnId, usage: result.usage };
1728
+ }
1340
1729
  const toolResults = ctx.toolExecution === "parallel" ? await executeToolsParallel(ctx, canonicalToolCalls, turnId) : await executeToolsSequential(ctx, canonicalToolCalls, turnId);
1341
1730
  const toolResultMsg = ctx.provider.toolResultsMessage(toolResults);
1342
1731
  ctx.turns.push({
@@ -1479,15 +1868,16 @@ async function executeSingleTool(ctx, call, turnId) {
1479
1868
  output = await toolDef.execute(effectiveInput, toolCtx);
1480
1869
  } catch (err) {
1481
1870
  const error = err instanceof Error ? err : new Error(String(err));
1482
- await ctx.hooks.callHook("tool:error", {
1871
+ const errorCtx = {
1483
1872
  turnId,
1484
1873
  callId,
1485
1874
  name: call.name,
1486
1875
  displayName,
1487
1876
  input: effectiveInput,
1488
1877
  error
1489
- });
1490
- output = `Tool error: ${error.message}`;
1878
+ };
1879
+ await ctx.hooks.callHook("tool:error", errorCtx);
1880
+ output = errorCtx.result ?? `Tool error: ${error.message}`;
1491
1881
  isError = true;
1492
1882
  }
1493
1883
  const transformCtx = {
@@ -1690,7 +2080,10 @@ function resolveBehavior(agentBehavior, runBehavior) {
1690
2080
  thinkingBudget: runBehavior?.thinkingBudget ?? agentBehavior?.thinkingBudget,
1691
2081
  schema: runBehavior?.schema ?? agentBehavior?.schema,
1692
2082
  cache: runBehavior?.cache ?? agentBehavior?.cache ?? true,
1693
- toolOutputBudget: runBehavior?.toolOutputBudget ?? agentBehavior?.toolOutputBudget
2083
+ toolOutputBudget: runBehavior?.toolOutputBudget ?? agentBehavior?.toolOutputBudget,
2084
+ compactStrategy: runBehavior?.compactStrategy ?? agentBehavior?.compactStrategy ?? "off",
2085
+ compactThreshold: runBehavior?.compactThreshold ?? agentBehavior?.compactThreshold,
2086
+ compactKeepTurns: runBehavior?.compactKeepTurns ?? agentBehavior?.compactKeepTurns
1694
2087
  };
1695
2088
  }
1696
2089
  function createAgent({ provider, name: agentName, system: agentSystem, tools: agentTools, toolAliases, behavior: agentBehavior, execution, mcpServers, session, skills: agentSkills, mcpConnector, eager }) {
@@ -1813,7 +2206,7 @@ function createAgent({ provider, name: agentName, system: agentSystem, tools: ag
1813
2206
  }
1814
2207
  const thinking = options.thinking ?? "off";
1815
2208
  const model = options.model ?? provider.meta.defaultModel;
1816
- const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget } = resolveBehavior(agentBehavior, options.behavior);
2209
+ const { toolExecution, maxTurns, maxTokens, thinkingBudget, schema, cache, toolOutputBudget, compactStrategy, compactThreshold, compactKeepTurns } = resolveBehavior(agentBehavior, options.behavior);
1817
2210
  let system = options.system || agentSystem || "You are a helpful assistant.";
1818
2211
  if (skillsCatalog) {
1819
2212
  system = `${system}
@@ -1949,6 +2342,9 @@ ${skillsCatalog}`;
1949
2342
  schema,
1950
2343
  cache,
1951
2344
  toolOutputBudget,
2345
+ compactStrategy,
2346
+ compactThreshold,
2347
+ compactKeepTurns,
1952
2348
  runStartMs
1953
2349
  });
1954
2350
  const finalStats = {
@@ -2393,7 +2789,7 @@ function createSpawnTool(options = {}) {
2393
2789
  var spawn = createSpawnTool();
2394
2790
 
2395
2791
  // src/tools/write-file.ts
2396
- import { Buffer as Buffer3 } from "buffer";
2792
+ import { Buffer as Buffer4 } from "buffer";
2397
2793
  var writeFile = {
2398
2794
  spec: {
2399
2795
  name: "write_file",
@@ -2415,7 +2811,7 @@ var writeFile = {
2415
2811
  existing = await ctx.execution.readFile(ctx.handle, targetPath);
2416
2812
  } catch {
2417
2813
  }
2418
- const bytes = Buffer3.byteLength(targetContent);
2814
+ const bytes = Buffer4.byteLength(targetContent);
2419
2815
  if (existing === targetContent)
2420
2816
  return `No change needed: ${targetPath} already at target state (${bytes} bytes).`;
2421
2817
  await ctx.execution.writeFile(ctx.handle, targetPath, targetContent);
@@ -176,6 +176,10 @@ function createProcessContext(config) {
176
176
  async readFile(handle, path) {
177
177
  return readFile(resolve(handle.cwd, path), "utf-8");
178
178
  },
179
+ async readFileBinary(handle, path) {
180
+ const buf = await readFile(resolve(handle.cwd, path));
181
+ return new Uint8Array(buf);
182
+ },
179
183
  async writeFile(handle, path, content) {
180
184
  const fullPath = resolve(handle.cwd, path);
181
185
  await mkdir(dirname(fullPath), { recursive: true });
@@ -1,6 +1,6 @@
1
- import { S as SpawnConfig, b as ExecutionContext } from './types-BpvTmawk.js';
2
- export { C as ContextCapabilities, a as ContextType, E as ExecResult, c as ExecutionHandle } from './types-BpvTmawk.js';
3
- export { S as SandboxProvider, c as createSandboxContext } from './sandbox-CW72eLDP.js';
1
+ import { S as SpawnConfig, b as ExecutionContext } from './types-vA1a_ZX7.js';
2
+ export { C as ContextCapabilities, a as ContextType, E as ExecResult, c as ExecutionHandle } from './types-vA1a_ZX7.js';
3
+ export { S as SandboxProvider, c as createSandboxContext } from './sandbox-CLghrTLi.js';
4
4
 
5
5
  /**
6
6
  * Docker execution context.