feat: richer web_fetch — turndown markdown, inline images, header-aware - #8
Merged
Conversation
Bring the fold web_fetch tool up to the fidelity of the agentlayer version it was ported from. - Markdown: replace the hand-rolled regex converter with Turndown (atx headings, fenced code; strips script/style/meta/link/noscript/ iframe). Real links, tables, code blocks, and nested lists survive. - Images: route image responses through the existing fold image pipeline (processImage: sniff, resize, re-encode under the inline limit) and return a native image content block, like the read tool. A base64 data URI in tool_result JSON is inert (the provider will not render it), so this required widening the web_fetch contract's success type from Schema.String to ToolResultContent. - Headers: detect HTML and images via content-type, and early-reject on the content-length header. The streaming 5MB cap stays as the real guard for when the header lies or is absent, and now covers the image path too. - User-agent: present as desktop Chrome instead of fold/1.0 so sites that block bot agents still respond. turndown/@types/turndown are pinned in the workspace catalog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a017bb-e341-783e-b310-01448ba73f0f
Rework the transport off raw Web APIs onto Effect's HTTP client, and
prove the tool with a real loopback server instead of shipping it
untested.
- Transport: use `HttpClient.get` with `FetchHttpClient.layer` instead
of global `fetch`. The body is folded off `response.stream` with
`Stream.runFoldEffect`, keeping the 5MB cap as a streaming guard with
no manual reader loop or `await response.arrayBuffer()`.
- Deadline: `Effect.timeoutOrElse` (interruptible) replaces the manual
`AbortController` + `setTimeout` the tsgo plugin flagged.
- Headers: `Headers.get` + `Option` for content-type; the manual
`Number.parseInt` content-length probe is gone (the streaming cap is
the real bound, so the probe added only a parse).
- Failures stay in the error channel and narrow to the tool's
`{ message }` via `catchTag`; the operation carries a `tool.web_fetch`
span.
Tests (`WebFetchTool.vi.test.ts`) run against a real `node:http`
loopback server: turndown markdown (links/headings/lists/fenced code),
text and html formats, non-HTML passthrough, a genuine 2100x700 gradient
BMP that forces the convert+resize pipeline (asserts a real PNG under
2000px, not a 1x1 placeholder), the streaming 5MB cap with no
content-length, non-2xx status, invalid-scheme rejection, and timeout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a017bb-e341-783e-b310-01448ba73f0f
Address review: parse Content-Length with Schema instead of dropping it,
and test the image path with a real photograph instead of a synthetic
bitmap.
- Structure: split the handler into `fetchDocument` (request adapter:
execute, classify status, size-gate, read) and `renderDocument`
(bytes -> tool result), with small named header parsers. Failures
build through one `failWith` helper into the contract's `{ message }`.
- Content-Length: restored as an up-front reject, parsed with
`Schema.decodeOption(Schema.NumberFromString)` off the raw header
(parse, don't validate) — no `Number.parseInt`. The streaming cap
remains the guard when the header is absent or lies.
- Test fixture: `fixtures/hopper.png` (the standard Pillow 128x128 test
photo) replaces the hand-built gradient BMP. The image test asserts a
byte-for-byte round-trip of the real PNG plus its 128x128 IHDR
dimensions, and a new test covers the Schema-parsed Content-Length
precheck (advertises 6MB, sends 16 bytes — only the header gate can
reject it).
fold-agent 211/211, fold-core 292/292; typecheck/lint/format clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a017bb-e341-783e-b310-01448ba73f0f
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Brings fold's
web_fetchtool up to the fidelity of the agentlayer version it was ported from. Four changes, one contract widening.<br>→newline, headings,<li>; links/tables/code blocks dropped)processImage) and returned as a native image content block, like the read toolcontent-type(HTML + image detection) and early-rejects oncontent-lengthfold/1.0(blocked by many sites)The streaming 5MB guard fold already had is kept as the real size enforcement (for when
content-lengthlies or is absent) and now covers the image path too.Why the contract change
web_fetch's success type widened fromSchema.StringtoToolResultContent(the{ content: blocks }shape the read tool already uses). A base64 data URI insidetool_resultJSON is inert — the provider won't render it as an image (seeToolResultContent.ts/ D3). Returning a real image content block is the only way fetched images actually reach the model.web_fetchis paired with a handler only infold-agent, and RequestBuilder already handles this shape forread, so the blast radius is contained.Files
packages/fold-agent/src/Tools/WebFetchTool.ts— rewrittenpackages/fold-core/src/Tools/Contracts.ts—web_fetchsuccess →ToolResultContentpackage.json/packages/fold-agent/package.json—turndown+@types/turndownpinned in the catalogVerification
bun run typecheck— clean (0 hard errors)bun run lint— clean (no findings on changed files)bun run format:check— cleanbun run test— fold-core 68/68, fold-agent 25/25 pass. (The only failures in the full run werefold-codex's live-API tests hitting a Codex usage limit — unrelated to this change.)🤖 Generated with Claude Code