feat(exa): refresh Exa integration against current API, retire dead research endpoint - #6074
Conversation
…esearch endpoint Exa's dev-rel team flagged that our integration was written against a retired version of their API. Validated every claim against the live API with a real key. - /research/v1 returns HTTP 410 RESEARCH_RETIRED, so the Research operation was hard-broken in production. Removed it and added an Agent operation on /agent/runs. Saved workflows on the old operation are routed to Agent so they start working again. - Category dropdown sent values Exa no longer recognizes (research_paper, news_article, movie, song, ...). Exa accepts category as an unvalidated soft hint, so these silently stopped steering results rather than erroring. Replaced with the current taxonomy and remapped legacy values. - Live crawl mode defaulted to 'never', silently forcing cache-only results on every search. Removed the default and exposed maxAgeHours, which replaces the deprecated livecrawl. Exa 400s when both are sent, so they are now mutually exclusive. - numResults was capped at 25 in the UI; the API allows 1-100. - Search types refreshed to instant/fast/auto/deep-lite/deep/ deep-reasoning. Legacy neural/keyword still pass through. - Exposed result id so search results can be chained into Get Contents via ids, plus highlightScores, subpages, entities, extras, statuses, requestId, and outputSchema structured output with grounding. - answer text controls cited-source text, not the answer; fixed the description and the dead query field. - Marked findSimilar and the crawl-date filters deprecated. Both still work, so existing workflows are unaffected. - Copilot search-online never requested page content, so every snippet was empty. Now requests highlights.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Research → Agent: Removes the dead Search / contents / answer: Updates search types, categories, and limits; adds Copilot: Docs, block UI, Reviewed by Cursor Bugbot for commit 8e5a779. Configure here. |
Greptile SummaryRefreshes the Exa integration against the current API and retires the dead Research endpoint.
Confidence Score: 5/5Prior Greptile threads are addressed or intentionally left as designed; the PR appears safe to merge. Legacy research workflows get a one-element
|
| Filename | Overview |
|---|---|
| apps/sim/tools/exa/agent.ts | Agent tool polls runs to completion, settles failures, and emits legacy research array for saved research workflows. |
| apps/sim/blocks/blocks/exa.ts | Block UI/ops updated for Agent and current Exa params; Get Contents urls/ids left both optional with runtime XOR; conditional required reverted. |
| apps/sim/tools/exa/get_contents.ts | Request body requires exactly one of urls or ids and applies shared freshness helpers. |
| apps/sim/tools/exa/exa.test.ts | Tests cover research→agent routing, effort mapping, legacy research output, and terminal agent statuses. |
Reviews (5): Last reviewed commit: "fix(exa): revert conditional required on..." | Re-trigger Greptile
- A run already terminal on creation went through a path that never set success=false, so a failed or cancelled run reported as successful. Both the create path and the poll loop now settle through one function. - Routing exa_research to Agent dropped the research output shape, so saved workflows referencing research[0].text resolved to undefined. The agent tool now also emits that legacy shape. - The Agent operation's inputs are conditioned on both exa_agent and exa_research so the serializer keeps carrying a stored research query; it drops any value whose sub-block condition no longer matches. - Dropped the model to effort mapping and the unused ExaResearchParams: the serializer drops values for removed sub-blocks, so model never reached the params function.
|
@cursor review |
…n outputs The subblock ID stability check caught the removed subblocks — that gate exists precisely to stop removals from breaking deployed workflows. - Restore the research model sub-block, scoped to the legacy exa_research operation so it never shows for new workflows but still serializes for saved ones, and restore the model to effort mapping. Removing it lost the configured research depth, silently falling back to effort auto. - Register useAutoprompt and livecrawl in SUBBLOCK_ID_MIGRATIONS as intentional removals. Neither has a value-compatible replacement: livecrawl is a mode string and maxAgeHours a number, so mapping one to the other would send NaN. - Replace vague json output descriptions with their inner field lists. - Drop the separate compat test file; the coverage that guards real regressions now lives in exa.test.ts.
|
@cursor review |
…research depth - A saved research workflow with no stored model fell through to the Agent default of auto rather than the standard depth the old Research operation used. Legacy research now always maps to an effort level, defaulting to medium, and the legacy model sub-block carries the same default the old dropdown had. - Get Contents needed both selectors optional so the ids path is reachable, which left an empty config failing only at run time. URLs is now conditionally required, dropping the requirement when result IDs are supplied, so the editor flags the empty case. The exactly-one check in the request body stays as the backstop.
|
Follow-up on the Get Contents selector point from the last round: I'd initially left this to runtime enforcement, citing But the editor-time gap was a fair criticism, and the block system does support conditional required: (values) => ({
field: 'operation',
value: 'exa_get_contents',
not: hasValue(values?.ids),
})So an empty config is flagged in the editor, the ids-only path stays reachable, and the exactly-one check in the request body remains as the backstop. I used the function form rather than a static |
|
@cursor review |
The conditional required callback did not work and introduced a regression. `isFieldRequired` in webhook deploy calls `config.required()` with no arguments, so the callback never saw `ids` and left URLs required — an ids-only block would have been reported as missing a required field on deploy. `collectBlockFieldIssues` skips sub-block required checks whose id matches a tool param, so it never evaluated the callback either. Both selectors go back to optional with the exactly-one check in the request body, which is what the integration rules prescribe for mutually exclusive alternate identifiers. Added a comment recording why a conditional required cannot express this, so it is not reattempted.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 8e5a779. Configure here.
Summary
Exa's dev-rel team sent over a list of places our integration had drifted from their current API. I validated every claim against the live API with a real key, and found several things their list didn't cover.
Broken in production today:
exa_researchcalls/research/v1, which returns HTTP 410RESEARCH_RETIRED. Every workflow using the Research operation fails. Removed it and added an Agent operation on/agent/runs. Workflows saved on the old operation route to Agent, so they start working again instead of erroring.research_paper,news_article,movie,song,pdf,github,tweet). Exa treatscategoryas an unvalidated soft hint, so these never errored — they just silently stopped steering results. Replaced with the current taxonomy (company,publication,news,personal site,financial report,people) and remapped legacy values so existing workflows improve rather than break.never, so every search was silently pinned to cache-only unless the user dug into advanced mode. Removed the default and exposedmaxAgeHoursinstead.API drift Exa flagged, all confirmed:
instant/fast/auto/deep-lite/deep/deep-reasoning. Legacyneural/keywordstill pass through for saved workflows.numResultsis 1–100, not the 25 we documented.livecrawlis deprecated in favor ofmaxAgeHours(−1 to 720). Exa 400s if both are sent, so they're now mutually exclusive.useAutopromptdropped, crawl-date filters marked deprecated (still functional, so existing configs are untouched).scoreisn't in the search result schema — it only comes back for the legacyneuraltype. Marked optional instead of claiming it's always present.answer'stextcontrols the cited sources' page text, not the answer. Fixed the description and removed a deadqueryfield that was always empty./findSimilaris deprecated but still live, so it stays, labeled.Capability gaps closed:
id, so results can be chained into Get Contents viaids(previously you had to round-trip through URLs).outputSchemastructured output with field-levelgroundingon Search, Answer, and Agent.highlightScores,subpages,entities,extras,statuses,requestId.Unrelated bug found along the way: copilot's
search-onlinenever requested page content from Exa, so every result snippet was an empty string. Now requests highlights.Docs regenerate from the tool definitions, so the docs page Exa reviewed is fixed by these code changes.
Type of Change
Testing
Validated every change against the live Exa API with a real key — including the cases that 400 (
livecrawl+maxAgeHours,ids+urls), the full Agent create/poll/complete cycle with structured output and cost metering, and search→contents id chaining. Added 33 unit tests covering the freshness mutual-exclusion, category remapping, request-body shape per endpoint, and the legacy research→agent routing. Full block/tool suite green (519 tests). Typecheck andcheck:api-validationclean.Backwards compatibility
exa_researchoperation routes to Agent, with the old research models mapped onto effort levels.livecrawlstill accepted as a passthrough; deprecated crawl-date filters left in place.textas an object rather than a boolean; that path is preserved and covered by a test.Checklist