Skip to content

feat: richer web_fetch — turndown markdown, inline images, header-aware - #8

Merged
K-Mistele merged 3 commits into
mainfrom
web-fetch-turndown-images
Aug 19, 2026
Merged

feat: richer web_fetch — turndown markdown, inline images, header-aware#8
K-Mistele merged 3 commits into
mainfrom
web-fetch-turndown-images

Conversation

@K-Mistele

Copy link
Copy Markdown
Contributor

What

Brings fold's web_fetch tool up to the fidelity of the agentlayer version it was ported from. Four changes, one contract widening.

# Before After
1. Markdown hand-rolled regex (<br>→newline, headings, <li>; links/tables/code blocks dropped) Turndown — atx headings, fenced code, strips script/style/meta/link/noscript/iframe
2. Images none (decoded as text) routed through the existing image pipeline (processImage) and returned as a native image content block, like the read tool
3. Headers ignored reads content-type (HTML + image detection) and early-rejects on content-length
4. User-agent fold/1.0 (blocked by many sites) desktop Chrome string

The streaming 5MB guard fold already had is kept as the real size enforcement (for when content-length lies or is absent) and now covers the image path too.

Why the contract change

web_fetch's success type widened from Schema.String to ToolResultContent (the { content: blocks } shape the read tool already uses). A base64 data URI inside tool_result JSON is inert — the provider won't render it as an image (see ToolResultContent.ts / D3). Returning a real image content block is the only way fetched images actually reach the model. web_fetch is paired with a handler only in fold-agent, and RequestBuilder already handles this shape for read, so the blast radius is contained.

Files

  • packages/fold-agent/src/Tools/WebFetchTool.ts — rewritten
  • packages/fold-core/src/Tools/Contracts.tsweb_fetch success → ToolResultContent
  • package.json / packages/fold-agent/package.jsonturndown + @types/turndown pinned in the catalog

Verification

  • bun run typecheck — clean (0 hard errors)
  • bun run lint — clean (no findings on changed files)
  • bun run format:check — clean
  • bun run testfold-core 68/68, fold-agent 25/25 pass. (The only failures in the full run were fold-codex's live-API tests hitting a Codex usage limit — unrelated to this change.)

🤖 Generated with Claude Code

K-Mistele and others added 3 commits August 18, 2026 20:14
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
@K-Mistele
K-Mistele merged commit c1a3879 into main Aug 19, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant