|
| 1 | +export const READ_PAGE_DEFAULT_LIMIT = 4000; |
| 2 | +export const READ_PAGE_MIN_LIMIT = 500; |
| 3 | +export const READ_PAGE_MAX_LIMIT = 6000; |
| 4 | + |
| 5 | +function boundedInteger(value, fallback, min, max) { |
| 6 | + if (value == null || value === '') return fallback; |
| 7 | + const numeric = Number(value); |
| 8 | + if (!Number.isFinite(numeric)) return fallback; |
| 9 | + return Math.min(max, Math.max(min, Math.floor(numeric))); |
| 10 | +} |
| 11 | + |
| 12 | +function withDeliveredText(result, text) { |
| 13 | + const originalLength = Number(result.originalLength) || 0; |
| 14 | + const textOffset = Number(result.textOffset) || 0; |
| 15 | + const returnedLength = text.length; |
| 16 | + const deliveredEnd = Math.min(originalLength, textOffset + returnedLength); |
| 17 | + const hasMore = deliveredEnd < originalLength; |
| 18 | + 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; |
| 26 | + return { |
| 27 | + ...result, |
| 28 | + text, |
| 29 | + returnedLength, |
| 30 | + textTruncated, |
| 31 | + hasMore, |
| 32 | + nextOffset: hasMore ? deliveredEnd : null, |
| 33 | + continuationArgs, |
| 34 | + truncationReason: textTruncated ? 'tool_output_window' : null, |
| 35 | + }; |
| 36 | +} |
| 37 | + |
| 38 | +function compactForm(form, inputLimit) { |
| 39 | + if (!form || typeof form !== 'object' || !Array.isArray(form.inputs)) return form; |
| 40 | + if (form.inputs.length <= inputLimit) return form; |
| 41 | + return { |
| 42 | + ...form, |
| 43 | + inputs: form.inputs.slice(0, inputLimit), |
| 44 | + inputsTruncated: true, |
| 45 | + originalInputCount: form.inputs.length, |
| 46 | + }; |
| 47 | +} |
| 48 | + |
| 49 | +function compactAuxiliaryContent(result, { |
| 50 | + linkLimit, |
| 51 | + formLimit, |
| 52 | + inputLimit, |
| 53 | + shadowLimit, |
| 54 | + mediaLimit, |
| 55 | +}) { |
| 56 | + const links = Array.isArray(result.links) ? result.links : null; |
| 57 | + const forms = Array.isArray(result.forms) ? result.forms : null; |
| 58 | + const shadowDOM = Array.isArray(result.shadowDOM) ? result.shadowDOM : null; |
| 59 | + const media = result.media && typeof result.media === 'object' ? result.media : null; |
| 60 | + const videos = Array.isArray(media?.videos) ? media.videos : null; |
| 61 | + const images = Array.isArray(media?.images) ? media.images : null; |
| 62 | + return { |
| 63 | + ...result, |
| 64 | + ...(links ? { |
| 65 | + links: links.slice(0, linkLimit), |
| 66 | + originalLinkCount: links.length, |
| 67 | + } : {}), |
| 68 | + ...(forms ? { |
| 69 | + forms: forms.slice(0, formLimit).map(form => compactForm(form, inputLimit)), |
| 70 | + originalFormCount: forms.length, |
| 71 | + } : {}), |
| 72 | + ...(shadowDOM ? { |
| 73 | + shadowDOM: shadowDOM.slice(0, shadowLimit), |
| 74 | + originalShadowRootCount: shadowDOM.length, |
| 75 | + } : {}), |
| 76 | + ...(media ? { |
| 77 | + media: { |
| 78 | + ...media, |
| 79 | + ...(videos ? { videos: videos.slice(0, mediaLimit) } : {}), |
| 80 | + ...(images ? { images: images.slice(0, mediaLimit) } : {}), |
| 81 | + }, |
| 82 | + } : {}), |
| 83 | + auxiliaryContentTruncated: true, |
| 84 | + }; |
| 85 | +} |
| 86 | + |
| 87 | +function compactCoreResult(result) { |
| 88 | + const shortString = (value, limit) => ( |
| 89 | + typeof value === 'string' ? value.slice(0, limit) : value |
| 90 | + ); |
| 91 | + const pageGate = result.pageGate && typeof result.pageGate === 'object' |
| 92 | + ? { |
| 93 | + type: shortString(result.pageGate.type, 100), |
| 94 | + blocking: result.pageGate.blocking === true, |
| 95 | + surface: shortString(result.pageGate.surface, 100), |
| 96 | + label: shortString(result.pageGate.label, 500), |
| 97 | + } |
| 98 | + : undefined; |
| 99 | + return withDeliveredText({ |
| 100 | + url: shortString(result.url, 1000), |
| 101 | + title: shortString(result.title, 500), |
| 102 | + text: result.text, |
| 103 | + textSource: shortString(result.textSource, 200), |
| 104 | + isArticlePage: result.isArticlePage === true, |
| 105 | + includeChrome: result.includeChrome === true, |
| 106 | + originalLength: result.originalLength, |
| 107 | + textOffset: result.textOffset, |
| 108 | + textLimit: result.textLimit, |
| 109 | + accessState: result.accessState, |
| 110 | + accessGateEvidence: result.accessGateEvidence, |
| 111 | + ...(pageGate ? { pageGate } : {}), |
| 112 | + auxiliaryContentTruncated: true, |
| 113 | + }, result.text); |
| 114 | +} |
| 115 | + |
| 116 | +export function isReadPageWindowResult(result) { |
| 117 | + return !!result |
| 118 | + && typeof result === 'object' |
| 119 | + && typeof result.text === 'string' |
| 120 | + && Number.isFinite(Number(result.originalLength)) |
| 121 | + && Number.isFinite(Number(result.textOffset)) |
| 122 | + && typeof result.accessState === 'string'; |
| 123 | +} |
| 124 | + |
| 125 | +export function applyReadPageWindow(result, args = {}) { |
| 126 | + if (!result || typeof result !== 'object' || typeof result.text !== 'string') return result; |
| 127 | + |
| 128 | + const originalText = result.text; |
| 129 | + const originalLength = originalText.length; |
| 130 | + const requestedOffset = boundedInteger(args.offset, 0, 0, Number.MAX_SAFE_INTEGER); |
| 131 | + const textOffset = Math.min(requestedOffset, originalLength); |
| 132 | + const textLimit = boundedInteger( |
| 133 | + args.limit, |
| 134 | + READ_PAGE_DEFAULT_LIMIT, |
| 135 | + READ_PAGE_MIN_LIMIT, |
| 136 | + READ_PAGE_MAX_LIMIT, |
| 137 | + ); |
| 138 | + const text = originalText.slice(textOffset, textOffset + textLimit); |
| 139 | + const blockingPageGate = result.pageGate?.blocking === true; |
| 140 | + const { |
| 141 | + media, |
| 142 | + activeElement, |
| 143 | + links, |
| 144 | + forms, |
| 145 | + shadowDOM, |
| 146 | + ...core |
| 147 | + } = result; |
| 148 | + |
| 149 | + return withDeliveredText({ |
| 150 | + ...core, |
| 151 | + text, |
| 152 | + originalLength, |
| 153 | + textOffset, |
| 154 | + textLimit, |
| 155 | + accessState: blockingPageGate ? 'blocked_by_page_gate' : 'no_blocking_page_gate', |
| 156 | + accessGateEvidence: blockingPageGate ? 'pageGate' : 'none', |
| 157 | + ...(media !== undefined ? { media } : {}), |
| 158 | + ...(activeElement !== undefined ? { activeElement } : {}), |
| 159 | + ...(links !== undefined ? { links } : {}), |
| 160 | + ...(forms !== undefined ? { forms } : {}), |
| 161 | + ...(shadowDOM !== undefined ? { shadowDOM } : {}), |
| 162 | + }, text); |
| 163 | +} |
| 164 | + |
| 165 | +export function fitReadPageWindowResult(result, maxChars = 8000) { |
| 166 | + if (!isReadPageWindowResult(result)) return result; |
| 167 | + const fits = candidate => JSON.stringify(candidate).length <= maxChars; |
| 168 | + if (fits(result)) return result; |
| 169 | + |
| 170 | + const profiles = [ |
| 171 | + { linkLimit: 20, formLimit: 5, inputLimit: 10, shadowLimit: 5, mediaLimit: 5 }, |
| 172 | + { linkLimit: 10, formLimit: 3, inputLimit: 5, shadowLimit: 2, mediaLimit: 2 }, |
| 173 | + { linkLimit: 0, formLimit: 0, inputLimit: 0, shadowLimit: 0, mediaLimit: 0 }, |
| 174 | + ]; |
| 175 | + let compact = result; |
| 176 | + for (const profile of profiles) { |
| 177 | + compact = compactAuxiliaryContent(result, profile); |
| 178 | + if (fits(compact)) return compact; |
| 179 | + } |
| 180 | + if (!fits(withDeliveredText(compact, ''))) { |
| 181 | + compact = compactCoreResult(result); |
| 182 | + } |
| 183 | + |
| 184 | + let low = 0; |
| 185 | + let high = compact.text.length; |
| 186 | + let best = withDeliveredText(compact, ''); |
| 187 | + while (low <= high) { |
| 188 | + const mid = Math.floor((low + high) / 2); |
| 189 | + const candidate = withDeliveredText(compact, compact.text.slice(0, mid)); |
| 190 | + if (fits(candidate)) { |
| 191 | + best = candidate; |
| 192 | + low = mid + 1; |
| 193 | + } else { |
| 194 | + high = mid - 1; |
| 195 | + } |
| 196 | + } |
| 197 | + return best; |
| 198 | +} |
0 commit comments