Skip to content

feat(search): keyless web research on Claude Code — claude-code SearchProvider#187

Merged
pgsharma merged 2 commits into
mainfrom
feat/claude-code-search
Jun 26, 2026
Merged

feat(search): keyless web research on Claude Code — claude-code SearchProvider#187
pgsharma merged 2 commits into
mainfrom
feat/claude-code-search

Conversation

@pgsharma

Copy link
Copy Markdown
Contributor

What

Follow-on to the keyless chat floor (#186). Web research no longer needs an Exa/Tavily key when running on Claude Code. A new ClaudeCodeSearch SearchProvider drives the Agent SDK's WebSearch tool and returns {url, title, snippet} hits that the existing _external_search rung in research/web.py grounds like any other provider — so a full report (Reddit demand + web) runs with zero keys.

Keyless search floor: resolve_search() falls back to ClaudeCodeSearch when no external search key is set and the SDK + CLI are present (mirrors the chat floor). Any search key still wins.

Verified live (no key): search("latest Python 3.14 release") → 3 validated real python.org/peps hits with snippets; resolve_search()ClaudeCodeSearch.

Why this design (not complete_grounded)

I dug into whether native web citations are reachable keyless — they're not: the SDK drops the Messages-API citations array (confirmed by probe and SDK issue #254). So GroundedResult span-citations are impossible without an API key. Instead I used the SearchProvider seamWebSearch returns real URLs, which the proven external-search grounding path already consumes. This is exactly how research agents (GPT-Researcher, Tavily/Exa pipelines) do it.

Grounding — no-cite-no-claim holds (URL-validated)

Attribution is reconstructed and validated, never trusted blind:

  1. Parse the real hit URLs out of the WebSearch tool-result (Links:[{title,url}]).
  2. Ask the model for structured {url,title,snippet} (SDK output_format json_schema).
  3. Drop any result whose URL isn't a real hit — an invented source can never enter the corpus. Empty/garbage → fall back to the bare real-hit URLs.

Binding is claim→URL (model-asserted, URL-verified), not native span-citation. Honest and labeled as such; EXA_API_KEY remains the stronger path.

Changes

  • search/adapters/claude_code.py (new) — ClaudeCodeSearch: agentic WebSearch + output_format, capture-real-URLs, validate, bare-URL fallback.
  • llm/adapters/_claude_code_runtime.py (new) — the shared async→sync bridge (background_loop + sdk_model) factored out of the chat adapter; the chat adapter now imports it (so both share one daemon loop, and the cross-module use isn't a private-access violation). No behavior change to the chat adapter — its 12 tests stay green.
  • config.py — keyless search floor in resolve_search().
  • tests (test_claude_code_search.py, new) — offline (fake SDK): keep-real/drop-invented URL validation, max_results, bare-URL fallback, MissingExtraError, floor engages/absent.
  • docs + CHANGELOG.

Gate

ruff check · ruff format --check · pyright (0 errors) · gen_ts_types.py --check PASSED · import metalworks stays free. 145 passed clean-room (search + web-research + config + chat + adapters). No version bump — tagging left to you.

🤖 Generated with Claude Code

…hProvider

Web research no longer needs an Exa/Tavily key when running on Claude Code. A new
`ClaudeCodeSearch` SearchProvider drives the Agent SDK's WebSearch tool and returns
{url,title,snippet} hits that the existing _external_search rung grounds like any
other provider — so a full report (Reddit demand + web) runs with zero keys.

Keyless search FLOOR: resolve_search() falls back to ClaudeCodeSearch when no
external search key is set and the SDK + CLI are present (mirrors the chat floor);
any search key still wins.

Grounding (no-cite-no-claim): native web-search citations are unreachable through
the SDK (it drops the Messages-API citations array — confirmed by probe + SDK issue
anthropics/claude-agent-sdk-typescript#254), so attribution is reconstructed and
URL-VALIDATED — the real hit URLs are parsed from the WebSearch tool-result, the
model returns structured {url,title,snippet}, and any result whose URL is not a real
hit is DROPPED. An invented source can never enter the corpus. Binding is claim→URL,
not native span-citation; set EXA_API_KEY for the stronger path.

- search/adapters/claude_code.py: ClaudeCodeSearch (WebSearch + output_format
  json_schema, URL-validate-against-real-hits, bare-URL fallback).
- llm/adapters/_claude_code_runtime.py (new): shared async→sync bridge
  (background_loop + sdk_model) — chat adapter refactored to use it, search adapter
  shares the one daemon loop. No behavior change to the chat adapter.
- config.py: keyless search floor in resolve_search().
- tests: offline (fake SDK) — keep-real/drop-invented URL validation, max_results,
  bare-URL fallback, MissingExtraError, floor engages/absent.
- docs + CHANGELOG. Verified live: keyless WebSearch → 3 validated real hits + floor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mintlify

mintlify Bot commented Jun 26, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
metalworks 🟢 Ready View Preview Jun 26, 2026, 1:20 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

Testing the keyless path as a real user would surfaced a stale hint: with no
provider key, `doctor`/`preflight` said "No provider key found → set one to run
the pipeline" and marked it an ERROR — but the claude-code chat floor means the
pipeline DOES run keyless now. The hint contradicted the resolved chat provider
(claude-code/sonnet) shown two lines above.

- preflight.py: when no provider key is set, branch on claude_code_available() —
  if present, a non-error hint ("running keyless on your Claude Code login; set a
  key for a faster path"); else the original hint, now also pointing at
  metalworks[claude-code] as the keyless option. The keyless hint doesn't start
  with "No provider key found", so _hint_severity keeps it a warning, not error.
- config.py: _claude_code_available → claude_code_available (public) — it's now
  used cross-module (preflight + config); pyright reportPrivateUsage forbids the
  private cross-module import. Callers + tests updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pgsharma pgsharma merged commit 32ba124 into main Jun 26, 2026
8 checks passed
@pgsharma pgsharma deleted the feat/claude-code-search branch June 26, 2026 02:01
pgsharma pushed a commit that referenced this pull request Jun 26, 2026
Cut 0.4.0: the keyless Claude Code path (chat floor #186, web-search floor #187,
caveat fix #188) plus the 0.3.1–0.3.3 reliability run, with a full docs/metadata
pass and a positioning refresh.

- Version → 0.4.0 (pyproject + __init__ + plugin.json) + CHANGELOG [Unreleased] → [0.4.0].
- Docs audit: surface the keyless `claude-code` path across README, docs/ (installation,
  index, quickstart, cli, claude-code, configuration, extending, demand-research, ai-agents,
  custom-chatmodel, internals), plugin docs + skills, CONTRIBUTING; add the `claude-code` extra
  everywhere it's listed; fix the stale "submissions come from the HF Parquet mirror" claim (the
  live Arctic Shift API has been the default since 0.1.1).
- Positioning: drop "marketing research" (markety) and de-narrow the whole-product framing from
  Reddit-only to "real conversations across the web (Reddit, HN, forums, …)"; refresh keywords
  (out: marketing; in: validation, startups, founders). Reddit kept where it's a genuine feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pgsharma added a commit that referenced this pull request Jun 26, 2026
Cut 0.4.0: the keyless Claude Code path (chat floor #186, web-search floor #187,
caveat fix #188) plus the 0.3.1–0.3.3 reliability run, with a full docs/metadata
pass and a positioning refresh.

- Version → 0.4.0 (pyproject + __init__ + plugin.json) + CHANGELOG [Unreleased] → [0.4.0].
- Docs audit: surface the keyless `claude-code` path across README, docs/ (installation,
  index, quickstart, cli, claude-code, configuration, extending, demand-research, ai-agents,
  custom-chatmodel, internals), plugin docs + skills, CONTRIBUTING; add the `claude-code` extra
  everywhere it's listed; fix the stale "submissions come from the HF Parquet mirror" claim (the
  live Arctic Shift API has been the default since 0.1.1).
- Positioning: drop "marketing research" (markety) and de-narrow the whole-product framing from
  Reddit-only to "real conversations across the web (Reddit, HN, forums, …)"; refresh keywords
  (out: marketing; in: validation, startups, founders). Reddit kept where it's a genuine feature.

Co-authored-by: Zpoof <praguns0726@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants