@vellumai/assistant 0.11.7-dev.202609010322.2bc955e → 0.11.7-dev.202609011017.3dd6343

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.7-dev.202609010322.2bc955e",
3
+ "version": "0.11.7-dev.202609011017.3dd6343",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -10,7 +10,8 @@ import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { afterEach, describe, expect, test } from "bun:test";
12
12
 
13
- import { THRESHOLD_CHARS } from "../context/post-turn-tool-result-truncation.js";
13
+ import { FILE_READ_TOOL_NAMES } from "../context/post-turn-tool-result-truncation.js";
14
+ import { RESULT_TIME_SPOOL_EXEMPT_TOOLS } from "../context/tool-result-spool.js";
14
15
  import {
15
16
  FileSystemOps,
16
17
  type PathPolicy,
@@ -330,8 +331,12 @@ describe("FileSystemOps.readFileSafe", () => {
330
331
  expect(body).toBe("abcd");
331
332
  });
332
333
 
333
- test("the read budget stays under the tool-result spool threshold", () => {
334
- expect(READ_CHAR_BUDGET).toBeLessThan(THRESHOLD_CHARS);
334
+ test("file reads are result-time spool-exempt so a full window survives its turn", () => {
335
+ // The budget exceeds the spool threshold, so without the exemption a
336
+ // default read of a large file would be stubbed before the model saw it.
337
+ for (const name of FILE_READ_TOOL_NAMES) {
338
+ expect(RESULT_TIME_SPOOL_EXEMPT_TOOLS.has(name)).toBe(true);
339
+ }
335
340
  });
336
341
  });
337
342
 
@@ -140,11 +140,12 @@ describe("spoolAndStubOversizedToolResults", () => {
140
140
  expect(existsSync(join(convDir, TOOL_RESULT_DIR))).toBe(false);
141
141
  });
142
142
 
143
- test("spools an oversized file read of an ordinary path", () => {
143
+ test("leaves an oversized file read of an ordinary path inline (explicit self-sized read)", () => {
144
144
  const blocks = [
145
145
  makeToolResult(LONG, "tu_read"),
146
146
  makeToolResult(LONG, "tu_host_read"),
147
147
  ];
148
+ const originals = blocks.map((b) => b);
148
149
 
149
150
  const count = spoolAndStubOversizedToolResults(blocks, {
150
151
  conversationDir: convDir,
@@ -154,13 +155,9 @@ describe("spoolAndStubOversizedToolResults", () => {
154
155
  : { name: "host_file_read", input: { path: "/Users/me/notes.md" } },
155
156
  });
156
157
 
157
- expect(count).toBe(2);
158
- expect((blocks[0] as { content: string }).content).toContain(
159
- TRUNCATION_MARKER,
160
- );
161
- expect((blocks[1] as { content: string }).content).toContain(
162
- TRUNCATION_MARKER,
163
- );
158
+ expect(count).toBe(0);
159
+ expect(blocks).toEqual(originals);
160
+ expect(existsSync(join(convDir, TOOL_RESULT_DIR))).toBe(false);
164
161
  });
165
162
 
166
163
  test("leaves a file read of a spooled .tool-results path inline", () => {
@@ -20,15 +20,22 @@ const AX_TREE_TAG = "<ax-tree>";
20
20
 
21
21
  /**
22
22
  * Tools whose results are explicit, self-sized reads the model paginates
23
- * itself: `web_fetch` takes `max_chars`/`start_index` and reports a
24
- * "Character Window: X-Y of Z", so the model pages by character index against
25
- * the window it requested. Stubbing such a result at result time hands the
26
- * model ~a tenth of the window it just sized — and it keeps paging blind
27
- * against content it never saw. Like the file-read tools, these explicit
28
- * reads are honored in full for the turn that made them; the post-turn pass
29
- * still truncates them at turn end, after the model has consumed the content.
23
+ * itself: each takes `max_chars`/`start_index` and reports where its window
24
+ * ended (`web_fetch`'s "Character Window: X-Y of Z", the file-read tools'
25
+ * "[Truncated: characters X-Y of Z]" notice), so the model pages by character
26
+ * index against the window it requested. Stubbing such a result at result
27
+ * time would hand the model a fraction of the window it just sized — and it
28
+ * keeps paging blind against content it never saw. These reads are honored in
29
+ * full for the turn that made them; the post-turn pass still truncates them
30
+ * at turn end, after the model has consumed the content. This exemption is
31
+ * also what lets `READ_CHAR_BUDGET` exceed `THRESHOLD_CHARS` without a fresh
32
+ * read being stubbed before the model sees it.
30
33
  */
31
- export const RESULT_TIME_SPOOL_EXEMPT_TOOLS = new Set<string>(["web_fetch"]);
34
+ export const RESULT_TIME_SPOOL_EXEMPT_TOOLS = new Set<string>([
35
+ "web_fetch",
36
+ "file_read",
37
+ "host_file_read",
38
+ ]);
32
39
 
33
40
  /**
34
41
  * Whether a tool result is eligible for the result-time spool/stub pass: the
@@ -39,12 +46,12 @@ export const RESULT_TIME_SPOOL_EXEMPT_TOOLS = new Set<string>(["web_fetch"]);
39
46
  * The spooled-read exemption is what keeps paging possible: a file read is the
40
47
  * model's only way to pull spooled content back into context, so stubbing a
41
48
  * read of a `.tool-results/` file would write a fresh copy and hand back
42
- * another stub, putting that content out of reach for good. It is scoped by
43
- * target path rather than by tool name, because a file read aimed anywhere
44
- * else is ordinary oversized output with no such circularity, and exempting it
45
- * keeps a whole file inline on every LLM call for the rest of the turn.
46
- * Explicit self-sized reads are honored in full; the post-turn pass still
47
- * truncates them at turn end, after the model has consumed the content.
49
+ * another stub, putting that content out of reach for good. The file-read
50
+ * tools are also name-exempt as explicit self-sized reads, but this
51
+ * path-scoped guarantee stands on its own so spooled content stays reachable
52
+ * regardless of how the exempt-tools set evolves. Explicit self-sized reads
53
+ * are honored in full; the post-turn pass still truncates them at turn end,
54
+ * after the model has consumed the content.
48
55
  */
49
56
  function isSpoolEligible(
50
57
  tr: ToolResultContent,
@@ -45,7 +45,7 @@ export const fileReadInputSchema = z.looseObject({
45
45
  max_chars: z
46
46
  .number()
47
47
  .describe(
48
- "Maximum number of characters to read. Defaults to 20000, which is also the ceiling. Text files only.",
48
+ "Maximum number of characters to read. Defaults to 100000, which is also the ceiling. Text files only.",
49
49
  )
50
50
  .optional()
51
51
  .catch(undefined),
@@ -61,7 +61,7 @@ export const fileReadInputSchema = z.looseObject({
61
61
  export const fileReadTool = {
62
62
  name: "file_read",
63
63
  description:
64
- "Read the contents of a file on your own machine. Text reads return the first 20000 characters unless you pass `max_chars`; when a read stops short the result says so, and `start_index` pages on from there. To find where something is in a large file, code_search is cheaper than paging through it. For image files (JPEG, PNG, GIF, WebP), returns the image for visual analysis. For audio files (MP3, WAV, OGG, FLAC, AAC, M4A), returns the audio for listening. Use host_file_read for files on your guardian's device instead.",
64
+ "Read the contents of a file on your own machine. Text reads return the first 100000 characters unless you pass `max_chars`; when a read stops short the result says so, and `start_index` pages on from there. To find where something is in a large file, code_search is cheaper than paging through it. For image files (JPEG, PNG, GIF, WebP), returns the image for visual analysis. For audio files (MP3, WAV, OGG, FLAC, AAC, M4A), returns the audio for listening. Use host_file_read for files on your guardian's device instead.",
65
65
  category: "filesystem",
66
66
  executionTarget: "sandbox",
67
67
  defaultRiskLevel: RiskLevel.Low,
@@ -53,7 +53,7 @@ export const hostFileReadInputSchema = z.looseObject({
53
53
  max_chars: z
54
54
  .number()
55
55
  .describe(
56
- "Maximum number of characters to read. Defaults to 20000, which is also the ceiling. Text files only.",
56
+ "Maximum number of characters to read. Defaults to 100000, which is also the ceiling. Text files only.",
57
57
  )
58
58
  .optional()
59
59
  .catch(undefined),
@@ -69,7 +69,7 @@ export const hostFileReadInputSchema = z.looseObject({
69
69
  export const hostFileReadTool = {
70
70
  name: "host_file_read",
71
71
  description:
72
- "Read the contents of a file on your guardian's device, including images (JPEG, PNG, GIF, WebP) and audio (MP3, WAV, OGG, FLAC, AAC, M4A). Text reads return the first 20000 characters unless you pass `max_chars`; when a read stops short the result says so, and `start_index` pages on from there. For files on your own machine, use file_read instead.",
72
+ "Read the contents of a file on your guardian's device, including images (JPEG, PNG, GIF, WebP) and audio (MP3, WAV, OGG, FLAC, AAC, M4A). Text reads return the first 100000 characters unless you pass `max_chars`; when a read stops short the result says so, and `start_index` pages on from there. For files on your own machine, use file_read instead.",
73
73
  category: "host-filesystem",
74
74
  executionTarget: "host",
75
75
  defaultRiskLevel: RiskLevel.Medium,
@@ -78,12 +78,15 @@ function pathError(
78
78
  }
79
79
 
80
80
  /**
81
- * Characters returned by a read that names no `max_chars`. Stays under
82
- * `THRESHOLD_CHARS` in `context/post-turn-tool-result-truncation.ts`, which
83
- * spools any larger tool result to disk and replaces it inline with a short
84
- * stub, so a default read returns content rather than a stub.
81
+ * Characters returned by a read that names no `max_chars`, and the ceiling for
82
+ * one that does. File reads are exempt from the result-time spool pass
83
+ * (`RESULT_TIME_SPOOL_EXEMPT_TOOLS` in `context/tool-result-spool.ts`), so the
84
+ * full window reaches the model for the turn that read it; the post-turn pass
85
+ * compacts oversized results to recoverable stubs at turn end, and the
86
+ * `tool-result-truncate` plugin bounds any single result against the model's
87
+ * context window.
85
88
  */
86
- export const READ_CHAR_BUDGET = 20_000;
89
+ export const READ_CHAR_BUDGET = 100_000;
87
90
 
88
91
  /**
89
92
  * Trailing marker appended when a read stops short of the end of the file. A
@@ -169,8 +172,8 @@ export class FileSystemOps {
169
172
  try {
170
173
  const raw = await readFile(filePath, "utf-8");
171
174
 
172
- // A ceiling, not just a default: a larger window would be spooled to
173
- // disk and replaced with a stub, returning less than this.
175
+ // A ceiling, not just a default: an uncapped `max_chars` would put an
176
+ // arbitrarily large file inline in a single result.
174
177
  const maxChars = Math.min(
175
178
  READ_CHAR_BUDGET,
176
179
  Math.max(0, input.maxChars ?? READ_CHAR_BUDGET),