@foldrun/core 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ // A tool result too long to send whole: kept in a file, previewed inline.
2
+ //
3
+ // A tool result stays in the conversation and is sent again on every later
4
+ // turn of the step, so one 20,000-character answer costs its tokens over and
5
+ // over (FOL-28: ~470k input tokens on a 3-line answer). Script and API tools
6
+ // used to cut at 20,000 characters and drop the rest — the model saw a
7
+ // fragment and could not ask for more. Now the whole output is written under
8
+ // the agent's `.cache/results/`, the model gets the head and the path, and
9
+ // reads on with Read (offset/limit) or Grep only if it needs to.
10
+ //
11
+ // `.cache/` is inside the workspace, so the file tools reach it, and is never
12
+ // copied back from a run's sandbox (run-container.ts DENY_BACK_CACHE): a
13
+ // result is the step's working material, not workspace content.
14
+
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import crypto from "node:crypto";
18
+
19
+ /** What the model sees inline, at most: about 3,000 tokens. */
20
+ export const RESULT_PREVIEW_CHARS = 12_000;
21
+
22
+ /** What a tool's output is collected up to before it is kept — enough for a
23
+ * big API page or a long script log, bounded so a runaway stream cannot
24
+ * fill the sandbox's memory. */
25
+ export const RESULT_KEEP_CHARS = 2_000_000;
26
+
27
+ /** The results folder, relative to the agent's working directory. */
28
+ export const RESULTS_DIR = ".cache/results";
29
+
30
+ /** What an agent that cannot read files gets: the whole result up to the
31
+ * old cut, as before 18 Oct 2026 — a pointer to a file it has no tool to
32
+ * open would only lose it the tail. */
33
+ export const UNREADABLE_CAP = 20_000;
34
+
35
+ /** Does a step granted these tools hold one that reads a kept result back? */
36
+ export function canReadBack(allowed: readonly string[]): boolean {
37
+ return allowed.includes("Read") || allowed.includes("Grep");
38
+ }
39
+
40
+ /** Where a long result may be kept: the agent's folder, and whether the agent
41
+ * holds a tool to read it back (Read or Grep). Asked per call, because the
42
+ * runner builds the tools before it settles the agent's grants. */
43
+ export interface KeepIn {
44
+ dir: string;
45
+ readable: () => boolean;
46
+ }
47
+
48
+ /**
49
+ * `text` as the model should see it: unchanged when short; otherwise its
50
+ * head — cut at a line break when one falls in the last fifth — and where
51
+ * the whole of it is. `label` names the file (the tool's name). An agent
52
+ * that cannot read files back, or no folder to keep it in: the whole result
53
+ * up to UNREADABLE_CAP, as it always was.
54
+ */
55
+ export function previewOrKeep(
56
+ text: string,
57
+ keep: KeepIn | null | undefined,
58
+ label: string,
59
+ cap: number = RESULT_PREVIEW_CHARS,
60
+ ): string {
61
+ const whole = () => (text.length > UNREADABLE_CAP ? `${text.slice(0, UNREADABLE_CAP)}\n…[truncated]` : text);
62
+ if (!keep || !keep.readable()) return whole();
63
+ const agentDir = keep.dir;
64
+ if (text.length <= cap) return text;
65
+ let cut = text.lastIndexOf("\n", cap);
66
+ if (cut < cap * 0.8) cut = cap;
67
+ const head = text.slice(0, cut);
68
+ const more = text.length - cut;
69
+ const name = `${label.replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 60)}-${Date.now().toString(36)}-${crypto.randomBytes(3).toString("hex")}.txt`;
70
+ const rel = path.posix.join(RESULTS_DIR, name);
71
+ try {
72
+ fs.mkdirSync(path.join(agentDir, RESULTS_DIR), { recursive: true });
73
+ fs.writeFileSync(path.join(agentDir, rel), text);
74
+ } catch {
75
+ // Nowhere to keep it: what it would have had before, rather than a tail lost.
76
+ return whole();
77
+ }
78
+ return (
79
+ `${head}\n…(${more} more characters not shown — the whole result, ${text.length} characters, is in \`${rel}\`. ` +
80
+ `Read it with offset/limit, or Grep it, if you need what is past this point.)`
81
+ );
82
+ }