Commit e8e3d69
feat(pi): optional multi-provider web search for the coding agent (#5951)
* feat(pi): optional multi-provider web search for the coding agent
Adds a search provider dropdown (Exa, Serper, Parallel, Firecrawl) to the Pi
block, off by default. The selected provider's key comes from the block field or
Workspace Settings → BYOK; a Sim-hosted key is never spent, so a missing key
fails the run with a setup message instead of quietly billing Sim.
Search is available in all three modes. Local Dev and Review Code register a
host-side tool that goes through the existing provider tools, while Create PR has
no host in the loop and gets a generated Pi extension in the sandbox. Both paths
derive their requests from one normalizer and are held together by a parity test,
since the sandbox copy cannot import Sim's code.
Results are normalized to title, URL, snippet, and publication date, capped per
field and per envelope, marked untrusted in the prompt, and limited to 20
searches per run so a tool loop cannot drain the workspace's quota.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(pi): drop the banned JSON round-trip from the search parity test
`check:utils` bans `JSON.parse(JSON.stringify(...))`. The round-trip was
normalizing the host body to its wire form, which buys nothing here: the bodies
are plain JSON and `toEqual` already ignores undefined members.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(pi): upgrade the E2B SDK so long Pi output streams stop failing
Create PR streams the whole Pi run through one Connect server-stream
(`commands.run` -> envd `Process.Start`), held open for the full
`PI_TIMEOUT_MS`. Mid-stream it could die with:
[internal] protocol error: received unsupported compressed output
That string is `@connectrpc/connect-web`, not Pi — Pi has no Connect
dependency at all. connect's `compressedFlag` is `0b00000001` and gzip's
magic first byte is `0x1f`; `0x1f & 0x01 === 1`, so a raw gzip body fed to
the envelope reader trips this on byte one. It reads as "the server sent a
compressed envelope" but really means "this was never a Connect envelope" —
an HTTP-level gzip that was not transparently decompressed.
e2b 2.30.0 pinned `@connectrpc/connect-web@2.0.0-rc.3` and drove envd
through undici 7 with `allowH2: true`. e2b 2.36.1 moves to stable
connect-web 2.1.2 and loads undici 8.8.0 when Node >= 22.19.0 — exactly our
engine floor — so the failing path gets a different HTTP stack.
The connect-web upgrade alone is not the fix: 2.0.0-rc.3 and 2.1.2 ship a
byte-identical `connect-transport.js` (bar the copyright year), and
connect-web still has no `acceptCompression` option by design. The undici 8
swap is the part that matters.
`@e2b/code-interpreter@2.7.0` only asks for `e2b: ^2.28.0`, so the override
pins the floor we actually need. Verified API-compatible: every method we
call (`Sandbox.create`, `runCode`, `commands.run`, `files.read/write`,
`kill`, `Template`, `defaultBuildLogger`, `waitForTimeout`) has an identical
signature across the two versions, and we never touch `SandboxPaginator`,
the one type that changed.
* fix(pi): correct search normalization edge cases and the budget's stated scope
Follow-ups from review of the web-search work. Each fix lands in both the host
adapter (`normalize.ts`) and the Create PR sandbox copy (`extension-source.ts`),
with the extension test asserting the two produce byte-identical envelopes.
- `usableUrl` was the one provider-controlled field not whitespace-bounded:
title/snippet/date all go through `collapseWhitespace`, `url` only trimmed.
Up to 2048 chars of newlines and control characters could ride into the
envelope. Dropped rather than collapsed — `url` must stay byte-exact to stay
resolvable, so collapsing would emit a different, still-dead link, and a URL
carrying raw whitespace is already malformed under RFC 3986.
- `numResults: null` (or `''`, or `[]`) returned 1 result, not the documented
default of 5: `Number(null)` is a finite 0, so the clamp floor won rather
than the default. Only a real number or a non-blank numeric string now counts
as the model having asked for a count.
- Envelope truncation was silent. When results were dropped to fit the 50 KB
ceiling the model read the short list as the complete answer. It now carries
a message saying so, and the message is inside what gets measured so the note
cannot push a truncated envelope back over the ceiling.
- The budget is per *block execution*, not per workflow run: the counter lives
in the tool spec and both adapters build a fresh one per execution, so a Pi
block inside a Loop gets the full allowance every iteration. The constant,
the agent-facing message, and the docs all claimed "per run". Renamed to
`PI_SEARCH_MAX_CALLS_PER_EXECUTION` and corrected the wording rather than
tightening the cap, since a shared ceiling would fail late iterations of a
legitimate fan-out.
- The Search API Key tooltip promised "switching providers clears this field".
That clear is driven through the collaborative editor setter, so a workflow
imported, forked, or updated via the API keeps the previous provider's key —
exactly the case where sending it to a new vendor matters.
Docs also gain a warning that Create PR hands both the model key and the search
key to the agent as environment variables, which Pi copies into every bash
child. That matters most for Settings > BYOK keys: those are workspace-scoped,
only admins can manage them, and the API only ever returns them masked — yet
anyone who can run a Pi block in Create PR mode can read the raw value.
* fix(pi): make the search provider drift guards actually fire
The "you cannot add a provider without mirroring it" story rested on two
mechanisms that did not hold. Verified by adding a fifth provider to
`PI_SEARCH_PROVIDERS` and running the build: it produced only two errors, and
every test still passed.
- `normalizePiSearchRecords` assigns to `let built` inside its switch rather
than returning, so unlike its two siblings a missing case was not a type
error — it silently normalized the new provider to zero results. Added an
explicit `never` check.
- The sandbox copy's `normalizeRecords` used a trailing `else` for Firecrawl,
so an unmirrored provider was silently normalized with Firecrawl's field
names; `extractRecords` did the same with its `payload.data` tail. Both now
test for `firecrawl` explicitly and throw otherwise.
- `Record<PiSearchProvider, ...>` on the `TOOLS` and `payloads` fixtures looked
like exhaustiveness guards but are inert: `apps/sim/tsconfig.json` excludes
`**/*.test.ts`, and vitest transpiles without typechecking. Both suites drive
their providers off `Object.keys(fixture)`, so a missing provider was skipped
rather than failed. Each suite now asserts its fixture covers the registry.
Re-running the same experiment now yields three compile errors plus two test
failures naming the missing fixtures.
* fix(pi): drop the workspace BYOK fallback for the search key
A fallback exists so a key has somewhere to go when the field is unavailable.
The Search API Key field is unconditionally available: unlike the model key,
whose visibility runs through `shouldRequireApiKeyForModel` and its `isHosted`
branch, `getSearchApiKeyCondition` gates only on whether a provider is
selected. So the fallback never had a configuration to cover.
Removing it also closes an escalation. Workspace BYOK keys are admin-managed
and the API only ever returns them masked, yet `resolvePiSearchKey` would
resolve one for any member who could run the block — and in Create PR that key
is handed to the sandbox as an environment variable, which Pi copies into every
bash child. A member could read a credential the product deliberately never
shows them. Requiring the key on the block keeps the sandbox exposure to a key
its author already holds.
Nothing depends on the fallback: it has never shipped.
- `resolvePiSearchKey` is now synchronous and returns the key, since there is
no lookup left to await. `byokProviderId` leaves the search registry and
`PiSearchKeySource` / `PiSearchKeyResolution` are gone — with one source,
`keySource` carried no information, and the logging rationale for it (a block
field silently shadowing a stored key) no longer exists.
- The field is now `required`. Safe alongside its condition: the serializer's
required check returns early for fields that are not visible, so a Pi block
with search off still validates. Pinned by a test.
Docs and the block's tooltip, placeholder, and best practices updated. The
Create PR key-exposure callout now explains the missing fallback rather than
recommending the block field as a way around it.
* docs(pi): import Callout explicitly, as the sibling block docs do
`fumadocs-ui/mdx`'s `defaultMdxComponents` already provides `Callout`, so the
callout added earlier rendered fine without this — but logs.mdx, credential.mdx,
and response.mdx all import it explicitly and pi.mdx was the outlier. Not a
build fix: the docs Vercel deployment is failing on staging HEAD as well.
* chore(deps): exclude the e2b packages from the release-age gate
CI's `bun install --frozen-lockfile` failed on the E2B upgrade:
error: No version matching "@e2b/code-interpreter" found for specifier
"^2.7.0" (blocked by minimum-release-age: 604800 seconds)
This did not reproduce locally because the checkout's bun was 1.2.15, which
predates `minimumReleaseAge` support and ignored the gate outright; CI runs the
pinned 1.3.13 and enforces it.
Excludes only the two packages that are actually too young —
@e2b/code-interpreter 2.7.0 (2026-07-23) and e2b 2.36.1 (2026-07-27). The rest
of the chain already clears the gate: @connectrpc/connect{,-web} 2.1.2 and
@bufbuild/protobuf 2.13.0 and undici 8.8.0 are all older than a week, and `tar`
resolves from the lockfile at 7.5.22 without needing an exception (the original
CI error named only @e2b/code-interpreter, and `bun install --frozen-lockfile
--ignore-scripts` under 1.3.13 now passes locally).
The lockfile is regenerated with bun 1.3.13 rather than 1.2.15, which also
corrects hoisting the older bun had gotten wrong on the merge commit: the root
`lucide-react` hoist moves from 1.23.0 back to 0.511.0 and `@radix-ui/react-slot`
from 1.3.0 to 1.2.2, each with the proper scoped entries. Package resolution
still differs from staging by exactly the e2b chain and nothing else.
Both entries age out on 2026-07-30 and 2026-08-03; drop them then.
---------
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>1 parent cb3611b commit e8e3d69
32 files changed
Lines changed: 3132 additions & 105 deletions
File tree
- apps
- docs/content/docs/en/workflows/blocks
- sim
- blocks/blocks
- executor/handlers/pi
- search
- lib/core/security
- tools/serper
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
40 | | - | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| |||
63 | 64 | | |
64 | 65 | | |
65 | 66 | | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
66 | 83 | | |
67 | 84 | | |
68 | 85 | | |
| |||
176 | 193 | | |
177 | 194 | | |
178 | 195 | | |
| 196 | + | |
179 | 197 | | |
180 | 198 | | |
181 | 199 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
56 | 82 | | |
57 | 83 | | |
58 | 84 | | |
59 | 85 | | |
60 | 86 | | |
61 | 87 | | |
62 | | - | |
| 88 | + | |
63 | 89 | | |
64 | 90 | | |
65 | 91 | | |
66 | 92 | | |
67 | 93 | | |
| 94 | + | |
68 | 95 | | |
69 | 96 | | |
70 | 97 | | |
| |||
122 | 149 | | |
123 | 150 | | |
124 | 151 | | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
125 | 183 | | |
126 | 184 | | |
127 | 185 | | |
| |||
434 | 492 | | |
435 | 493 | | |
436 | 494 | | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
437 | 500 | | |
438 | 501 | | |
439 | 502 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| 35 | + | |
34 | 36 | | |
35 | 37 | | |
36 | 38 | | |
| |||
43 | 45 | | |
44 | 46 | | |
45 | 47 | | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
46 | 55 | | |
47 | 56 | | |
48 | 57 | | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
49 | 69 | | |
50 | 70 | | |
51 | 71 | | |
| |||
56 | 76 | | |
57 | 77 | | |
58 | 78 | | |
| 79 | + | |
59 | 80 | | |
60 | 81 | | |
61 | 82 | | |
| |||
0 commit comments