Skip to content

Commit 75effb0

Browse files
committed
Preserve read_page continuation options
1 parent f47be86 commit 75effb0

5 files changed

Lines changed: 42 additions & 8 deletions

File tree

src/chrome/src/agent/read-page-window.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,21 @@ function withDeliveredText(result, text) {
1616
const deliveredEnd = Math.min(originalLength, textOffset + returnedLength);
1717
const hasMore = deliveredEnd < originalLength;
1818
const textTruncated = textOffset > 0 || hasMore;
19+
const continuationArgs = hasMore
20+
? {
21+
offset: deliveredEnd,
22+
limit: Number(result.textLimit) || READ_PAGE_DEFAULT_LIMIT,
23+
includeChrome: result.includeChrome === true,
24+
}
25+
: null;
1926
return {
2027
...result,
2128
text,
2229
returnedLength,
2330
textTruncated,
2431
hasMore,
2532
nextOffset: hasMore ? deliveredEnd : null,
33+
continuationArgs,
2634
truncationReason: textTruncated ? 'tool_output_window' : null,
2735
};
2836
}

src/chrome/src/agent/tools.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export const AGENT_TOOLS = [
129129
type: 'function',
130130
function: {
131131
name: 'read_page',
132-
description: 'Read the current page as a bounded PROSE window — title, URL, visible text, links, and forms. LEGACY read path; prefer get_accessibility_tree for UI tasks. Use read_page only for long-form text content (articles, READMEs, documentation). Continue long text with `offset: nextOffset` while `hasMore:true`; do not scroll and reread the same document prefix. RESULT SHAPE: `text`, `originalLength`, `textOffset`, `textLimit`, `returnedLength`, `textTruncated`, `hasMore`, and `nextOffset` describe the tool-output window. `truncationReason:"tool_output_window"` is a context-window boundary, never evidence of a paywall. `accessState:"blocked_by_page_gate"` plus `accessGateEvidence:"pageGate"` is the structured access-block signal; `accessState:"no_blocking_page_gate"` means tool truncation must not be described as an access restriction. `pageGate`, when present, describes the rendered blocking surface; `textSource` identifies the article selector or bounded pre-gate/gate text; `isArticlePage` reports article markup. NOTE: PDF tabs auto-redirect to read_pdf because Chrome\'s PDF viewer is a chrome-extension:// page that content scripts cannot scrape.',
132+
description: 'Read the current page as a bounded PROSE window — title, URL, visible text, links, and forms. LEGACY read path; prefer get_accessibility_tree for UI tasks. Use read_page only for long-form text content (articles, READMEs, documentation). While `hasMore:true`, continue with the exact returned `continuationArgs`; it carries `offset:nextOffset`, `limit`, and extraction options such as `includeChrome`. Do not scroll and reread the same document prefix. RESULT SHAPE: `text`, `originalLength`, `textOffset`, `textLimit`, `returnedLength`, `textTruncated`, `hasMore`, `nextOffset`, and `continuationArgs` describe the tool-output window. `truncationReason:"tool_output_window"` is a context-window boundary, never evidence of a paywall. `accessState:"blocked_by_page_gate"` plus `accessGateEvidence:"pageGate"` is the structured access-block signal; `accessState:"no_blocking_page_gate"` means tool truncation must not be described as an access restriction. `pageGate`, when present, describes the rendered blocking surface; `textSource` identifies the article selector or bounded pre-gate/gate text; `isArticlePage` reports article markup. NOTE: PDF tabs auto-redirect to read_pdf because Chrome\'s PDF viewer is a chrome-extension:// page that content scripts cannot scrape.',
133133
parameters: {
134134
type: 'object',
135135
properties: {
@@ -140,7 +140,7 @@ export const AGENT_TOOLS = [
140140
offset: {
141141
type: 'integer',
142142
minimum: 0,
143-
description: 'Character offset into the extracted prose. Default 0. When hasMore is true, call read_page again with offset set exactly to nextOffset.',
143+
description: 'Character offset into the extracted prose. Default 0. When hasMore is true, pass the exact returned continuationArgs so extraction options stay stable.',
144144
},
145145
limit: {
146146
type: 'integer',
@@ -1414,7 +1414,7 @@ READING THE CURRENT TAB vs. FETCHING URLS — read this:
14141414
- DO NOT call \`fetch_url\` or \`research_url\` against the URL of the active tab, the API equivalent of the active tab, or a "renderable" / "raw" / "amp" / "mobile" variant of the active tab's URL. Re-fetching content the user is already looking at is the most common wasted step. Symptom of this antipattern: you fetch a Wikipedia/MediaWiki API URL for the same page the user is on, get a truncated result, then fetch a slightly different variant hoping for more content. Stop and call \`read_page\` instead.
14151415
- \`fetch_url\` and \`research_url\` are for content on OTHER URLs — a referenced article, an API the page links to, a sibling page, a different site entirely.
14161416
- If \`get_accessibility_tree({filter:"visible"})\` returns \`truncated:true\` / \`hasMore:true\`, call \`get_accessibility_tree({filter:"visible", page: nextPage})\` before scrolling to find a control that may already be visible but omitted from the first chunk.
1417-
- If \`read_page\` returns \`hasMore:true\`, continue deterministically with \`read_page({offset: nextOffset})\` until enough article text is covered. Do not scroll and reread the same prefix. \`truncationReason:"tool_output_window"\` with \`accessState:"no_blocking_page_gate"\` is NOT a paywall or access restriction; only a structured blocking \`pageGate\` supports that claim.
1417+
- If \`read_page\` returns \`hasMore:true\`, continue deterministically with the exact returned \`continuationArgs\` (equivalent to \`{offset: nextOffset, limit: textLimit, includeChrome}\`) until enough article text is covered. Preserve every extraction option across windows; do not scroll and reread the same prefix. \`truncationReason:"tool_output_window"\` with \`accessState:"no_blocking_page_gate"\` is NOT a paywall or access restriction; only a structured blocking \`pageGate\` supports that claim.
14181418
14191419
Guidelines:
14201420
1. Read the page first (a11y tree by default) to understand the context, then answer the user's question.

src/firefox/src/agent/read-page-window.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,21 @@ function withDeliveredText(result, text) {
1616
const deliveredEnd = Math.min(originalLength, textOffset + returnedLength);
1717
const hasMore = deliveredEnd < originalLength;
1818
const textTruncated = textOffset > 0 || hasMore;
19+
const continuationArgs = hasMore
20+
? {
21+
offset: deliveredEnd,
22+
limit: Number(result.textLimit) || READ_PAGE_DEFAULT_LIMIT,
23+
includeChrome: result.includeChrome === true,
24+
}
25+
: null;
1926
return {
2027
...result,
2128
text,
2229
returnedLength,
2330
textTruncated,
2431
hasMore,
2532
nextOffset: hasMore ? deliveredEnd : null,
33+
continuationArgs,
2634
truncationReason: textTruncated ? 'tool_output_window' : null,
2735
};
2836
}

src/firefox/src/agent/tools.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export const AGENT_TOOLS = [
129129
type: 'function',
130130
function: {
131131
name: 'read_page',
132-
description: 'Read the current page as a bounded PROSE window — title, URL, visible text, links, and forms. LEGACY read path; prefer get_accessibility_tree for UI tasks. Use read_page only for long-form text content (articles, READMEs, documentation). Continue long text with `offset: nextOffset` while `hasMore:true`; do not scroll and reread the same document prefix. RESULT SHAPE: `text`, `originalLength`, `textOffset`, `textLimit`, `returnedLength`, `textTruncated`, `hasMore`, and `nextOffset` describe the tool-output window. `truncationReason:"tool_output_window"` is a context-window boundary, never evidence of a paywall. `accessState:"blocked_by_page_gate"` plus `accessGateEvidence:"pageGate"` is the structured access-block signal; `accessState:"no_blocking_page_gate"` means tool truncation must not be described as an access restriction. `pageGate`, when present, describes the rendered blocking surface; `textSource` identifies the article selector or bounded pre-gate/gate text; `isArticlePage` reports article markup. NOTE: PDF tabs auto-redirect to read_pdf because Firefox\'s built-in viewer is a privileged page that content scripts cannot scrape.',
132+
description: 'Read the current page as a bounded PROSE window — title, URL, visible text, links, and forms. LEGACY read path; prefer get_accessibility_tree for UI tasks. Use read_page only for long-form text content (articles, READMEs, documentation). While `hasMore:true`, continue with the exact returned `continuationArgs`; it carries `offset:nextOffset`, `limit`, and extraction options such as `includeChrome`. Do not scroll and reread the same document prefix. RESULT SHAPE: `text`, `originalLength`, `textOffset`, `textLimit`, `returnedLength`, `textTruncated`, `hasMore`, `nextOffset`, and `continuationArgs` describe the tool-output window. `truncationReason:"tool_output_window"` is a context-window boundary, never evidence of a paywall. `accessState:"blocked_by_page_gate"` plus `accessGateEvidence:"pageGate"` is the structured access-block signal; `accessState:"no_blocking_page_gate"` means tool truncation must not be described as an access restriction. `pageGate`, when present, describes the rendered blocking surface; `textSource` identifies the article selector or bounded pre-gate/gate text; `isArticlePage` reports article markup. NOTE: PDF tabs auto-redirect to read_pdf because Firefox\'s built-in viewer is a privileged page that content scripts cannot scrape.',
133133
parameters: {
134134
type: 'object',
135135
properties: {
@@ -140,7 +140,7 @@ export const AGENT_TOOLS = [
140140
offset: {
141141
type: 'integer',
142142
minimum: 0,
143-
description: 'Character offset into the extracted prose. Default 0. When hasMore is true, call read_page again with offset set exactly to nextOffset.',
143+
description: 'Character offset into the extracted prose. Default 0. When hasMore is true, pass the exact returned continuationArgs so extraction options stay stable.',
144144
},
145145
limit: {
146146
type: 'integer',
@@ -1289,7 +1289,7 @@ READING THE CURRENT TAB vs. FETCHING URLS — read this:
12891289
- DO NOT call \`fetch_url\` or \`research_url\` against the URL of the active tab, the API equivalent of the active tab, or a "renderable" / "raw" / "amp" / "mobile" variant of the active tab's URL. Re-fetching content the user is already looking at is the most common wasted step. Symptom of this antipattern: you fetch a Wikipedia/MediaWiki API URL for the same page the user is on, get a truncated result, then fetch a slightly different variant hoping for more content. Stop and call \`read_page\` instead.
12901290
- \`fetch_url\` and \`research_url\` are for content on OTHER URLs — a referenced article, an API the page links to, a sibling page, a different site entirely.
12911291
- If \`get_accessibility_tree({filter:"visible"})\` returns \`truncated:true\` / \`hasMore:true\`, call \`get_accessibility_tree({filter:"visible", page: nextPage})\` before scrolling to find a control that may already be visible but omitted from the first chunk.
1292-
- If \`read_page\` returns \`hasMore:true\`, continue deterministically with \`read_page({offset: nextOffset})\` until enough article text is covered. Do not scroll and reread the same prefix. \`truncationReason:"tool_output_window"\` with \`accessState:"no_blocking_page_gate"\` is NOT a paywall or access restriction; only a structured blocking \`pageGate\` supports that claim.
1292+
- If \`read_page\` returns \`hasMore:true\`, continue deterministically with the exact returned \`continuationArgs\` (equivalent to \`{offset: nextOffset, limit: textLimit, includeChrome}\`) until enough article text is covered. Preserve every extraction option across windows; do not scroll and reread the same prefix. \`truncationReason:"tool_output_window"\` with \`accessState:"no_blocking_page_gate"\` is NOT a paywall or access restriction; only a structured blocking \`pageGate\` supports that claim.
12931293
12941294
Guidelines:
12951295
1. Read the page first to understand the context, then answer the user's question.

test/run.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3339,12 +3339,30 @@ test('read_page paginates a long friend-link article without inventing a paywall
33393339
assert.equal(page.accessState, 'no_blocking_page_gate', `${label}: readable friend link was treated as blocked`);
33403340
assert.equal(page.accessGateEvidence, 'none', `${label}: tool truncation invented access-gate evidence`);
33413341
assert.equal(page.truncationReason, 'tool_output_window', `${label}: long page lacks a structured tool-window reason`);
3342+
if (page.hasMore) {
3343+
assert.deepEqual(page.continuationArgs, {
3344+
offset: page.nextOffset,
3345+
limit: 3000,
3346+
includeChrome: false,
3347+
}, `${label}: continuation does not preserve extraction options`);
3348+
}
33423349
offset = page.nextOffset;
33433350
} while (offset !== null);
33443351

33453352
assert.equal(pages.map(page => page.text).join(''), longText, `${label}: continuation skipped or duplicated article text`);
33463353
assert.equal(pages.at(-1).hasMore, false, `${label}: final window still claims more text`);
33473354
assert.equal(pages.at(-1).nextOffset, null, `${label}: final window exposed a stale continuation`);
3355+
assert.equal(pages.at(-1).continuationArgs, null, `${label}: final window exposed stale continuation arguments`);
3356+
3357+
const chromeIncluded = runtime.applyReadPageWindow({
3358+
...friendLinkArticle,
3359+
includeChrome: true,
3360+
}, { includeChrome: true, limit: 3000 });
3361+
assert.deepEqual(chromeIncluded.continuationArgs, {
3362+
offset: 3000,
3363+
limit: 3000,
3364+
includeChrome: true,
3365+
}, `${label}: includeChrome was lost across continuation`);
33483366

33493367
const blocked = runtime.applyReadPageWindow({
33503368
...friendLinkArticle,
@@ -3376,9 +3394,9 @@ test('read_page schema and prompts require nextOffset continuation in both brows
33763394
assert.equal(properties.offset.minimum, 0, `${label}: offset accepts negative values`);
33773395
assert.equal(properties.limit.minimum, 500, `${label}: limit minimum drifted`);
33783396
assert.equal(properties.limit.maximum, 6000, `${label}: limit maximum drifted`);
3379-
assert.match(tool.function.description, /offset: nextOffset[\s\S]*hasMore:true/i, `${label}: tool does not teach continuation`);
3397+
assert.match(tool.function.description, /hasMore:true[\s\S]*continuationArgs[\s\S]*offset:nextOffset/i, `${label}: tool does not teach stable continuation`);
33803398
assert.match(tool.function.description, /tool_output_window[\s\S]*never evidence of a paywall/i, `${label}: tool truncation can be mistaken for a paywall`);
3381-
assert.match(prompt, /read_page\(\{offset: nextOffset\}\)/, `${label}: Ask prompt does not continue the returned window`);
3399+
assert.match(prompt, /continuationArgs[\s\S]*offset: nextOffset[\s\S]*includeChrome/, `${label}: Ask prompt does not preserve continuation options`);
33823400
assert.match(prompt, /no_blocking_page_gate[\s\S]*NOT a paywall/i, `${label}: Ask prompt lacks the structural access distinction`);
33833401
assert.doesNotMatch(prompt, /scroll the tab and re-read/i, `${label}: stale scroll-and-reread guidance survived`);
33843402
}

0 commit comments

Comments
 (0)