Skip to content

Commit e8e3d69

Browse files
BillLeoutsakosvl346Bill Leoutsakoscursoragenticecrasher321
authored
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

apps/docs/content/docs/en/workflows/blocks/pi.mdx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ description: The Pi Coding Agent block runs an autonomous coding agent on a real
44
pageType: reference
55
---
66

7+
import { Callout } from 'fumadocs-ui/components/callout'
78
import { BlockPreview } from '@/components/workflow-preview'
89
import { FAQ } from '@/components/ui/faq'
910

@@ -37,7 +38,7 @@ Review Code uses a disposable sandbox for the repository, but the Pi harness and
3738
- Requires sandbox execution. The provider key stays in Sim, so hosted keys and BYOK are both supported.
3839
- Needs a **GitHub token** that can clone the repo and submit reviews (see [Setup](#setup-cloud-code-review)).
3940
- Needs the **Pull Request Number** to review.
40-
- Does not load skills or memory, and never exposes shell, write, edit, or arbitrary network tools to the reviewer.
41+
- Does not load skills or memory, and never exposes shell, write, or edit tools to the reviewer. Its only network access is [Internet Search](#internet-search), and only when you select a provider.
4142
- Rechecks the PR immediately before submission and pins the review to the exact checked-out head commit.
4243
- The deliverable is a **submitted review** — read `reviewUrl` and `commentsPosted`.
4344

@@ -63,6 +64,22 @@ The model that drives the agent. Defaults to `claude-sonnet-4-6`. The dropdown c
6364

6465
Your key for the chosen provider. On hosted Sim it is optional for Local Dev and Review Code runs (a hosted key is used and metered to your workspace). **Create PR requires your own key** because its model client runs in the sandbox. When the provider supports workspace BYOK, you can store the key in **Settings → BYOK** instead of entering it on the block.
6566

67+
### Internet Search
68+
69+
Off by default. Pick a provider — **Exa**, **Serper**, **Parallel AI**, or **Firecrawl** — and the agent gains a single `web_search` tool that returns a handful of results, each with a title, URL, snippet, and (where the provider reports one) a publication date. It works the same way in all three modes, and it is the agent's only network access in Review Code. The tool accepts at most 20 calls **per block execution**, which bounds accidental tool loops. A Pi block inside a Loop or Parallel gets that allowance again on every iteration, so bound the iteration count too if you care about what a single workflow run can spend.
70+
71+
Search always uses **your own key** for the selected provider, entered in the block's **Search API Key** field. That field is the only source: there is no workspace BYOK fallback and Sim never supplies a hosted search key, so unlike the model key this field appears on every deployment. Leave it empty and the run fails with a setup error before any sandbox is created. Changing the provider in the editor clears the field, so re-enter the key that belongs to the provider you picked — a workflow you import, fork, or update through the API keeps whatever key was saved, so check it there.
72+
73+
<Callout type="warn">
74+
**Create PR exposes both keys to the agent.** Create PR runs the model client and the search client *inside* the sandbox, so the model key and the search key reach it as environment variables — and Pi copies its own environment into every shell command it runs. Your prompt, or instructions injected through the contents of the cloned repository, can therefore read either key and write it anywhere the agent can reach, including into the pull request itself. Sim strips verbatim key text out of run output, but that does not stop an agent that encodes the value first.
75+
76+
This is why the search key has no **Settings → BYOK** fallback. Workspace BYOK keys belong to the workspace rather than to you — Sim only ever displays them masked, and only workspace admins can add or remove them — so resolving one here would let anyone who can run a Pi block read a credential they cannot otherwise see. Requiring the key on the block keeps the exposure to a key its author already holds. Scope it to something you are willing to rotate.
77+
</Callout>
78+
79+
Results are third-party data. The agent is instructed to treat them as quoted evidence and never to follow instructions found inside them — the same posture Pi takes toward repository contents.
80+
81+
Traffic goes both ways: the agent writes its own queries after reading the repository, so leave search on **None** in Review Code when the pull request comes from an untrusted fork of a private repo. Injected instructions in a diff could otherwise put repository text into a query sent to the provider.
82+
6683
### Repository (Create PR / Review Code)
6784

6885
- **Repository Owner / Repository Name** — the GitHub repo (for example `your-org` / `your-repo`).
@@ -176,6 +193,7 @@ Enable sandbox execution as for Create PR. BYOK is optional because the model cr
176193
{ question: "Why does Local Dev need a public hostname?", answer: "Sim connects over raw SSH and blocks localhost, LAN, and private/reserved addresses for safety. Expose the machine with a TCP tunnel such as `ngrok tcp 22` and use the tunnel's host and port. Tailscale's private 100.x addresses won't work for the same reason." },
177194
{ question: "What GitHub permissions does Create PR need?", answer: "A token that can clone, push, and open a PR. With a fine-grained token: select the repo and grant Contents: Read and write plus Pull requests: Read and write. With a classic token: the repo scope. For organization repos, the token must be SSO-authorized." },
178195
{ question: "What GitHub permissions does Review Code need?", answer: "A token that can clone the repo and submit a review. With a fine-grained token: Contents: Read plus Pull requests: Read and write. Push permission is not required. With a classic token: the repo scope. For organization repos, the token must be SSO-authorized." },
196+
{ question: "Can the agent search the web?", answer: "Only if you pick a provider under Internet Search — Exa, Serper, Parallel AI, or Firecrawl. That adds one web_search tool in every mode, backed by your own key for that provider, entered on the block; there is no BYOK fallback and Sim never supplies a search key. Leave it on None and the agent has no search tool at all." },
179197
{ question: "Can I give it Gmail, Slack, or other integrations?", answer: "Yes, in Local Dev via the Tools field. Selected Sim tools run through Sim with your connected credentials, the same as the Agent block, so the agent can act beyond the repo while it codes. MCP and custom tools aren't supported yet." },
180198
{ question: "Where do the changes or feedback go?", answer: "In Create PR, to a new branch and a pull request (read prUrl and branch). In Review Code, to a submitted GitHub review on the existing PR (read reviewUrl and commentsPosted). In Local Dev, the files are edited in place on the target machine — review them with git there. Create PR and Local Dev also return changedFiles and a diff." },
181199
{ question: "What happens when memory or context gets large?", answer: "For Create PR and Local Dev, Sim trims memory before the run based on the memory type, and Pi compacts older turns as needed. Review Code does not load or save memory because a malicious PR could otherwise expose or poison prior context." },

apps/sim/blocks/blocks/pi.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it, vi } from 'vitest'
5+
6+
// The registry lives server-side (`keys.ts` reaches BYOK, which reaches the database) and the block
7+
// deliberately does not import it — no block imports from `@/executor`. This test is what ties the
8+
// two copies together, so adding a provider to one and not the other fails here.
9+
vi.mock('@/lib/api-key/byok', () => ({ getBYOKKey: vi.fn(), getApiKeyWithBYOK: vi.fn() }))
10+
11+
import { evaluateSubBlockCondition } from '@/lib/workflows/subblocks/visibility'
12+
import { PiBlock } from '@/blocks/blocks/pi'
13+
import { PI_SEARCH_PROVIDERS } from '@/executor/handlers/pi/keys'
14+
15+
const searchProviderField = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'searchProvider')
16+
const searchApiKeyField = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'searchApiKey')
17+
18+
function searchKeyVisible(values: Record<string, unknown>): boolean {
19+
return evaluateSubBlockCondition(searchApiKeyField?.condition, values)
20+
}
21+
22+
describe('Pi block search fields', () => {
23+
it('offers None plus exactly the providers the resolver knows, defaulting to None', () => {
24+
expect(searchProviderField?.type).toBe('dropdown')
25+
expect(searchProviderField?.defaultValue).toBe('none')
26+
27+
const options = searchProviderField?.options as { id: string; label: string }[]
28+
expect(options.map(({ id }) => id)).toEqual(['none', ...Object.keys(PI_SEARCH_PROVIDERS)])
29+
// Labels too: a mismatch here means the dropdown names a provider differently from the setup
30+
// error the run fails with.
31+
expect(options.slice(1).map(({ label }) => label)).toEqual(
32+
Object.values(PI_SEARCH_PROVIDERS).map(({ label }) => label)
33+
)
34+
})
35+
36+
// The same handling this block already gives githubToken, password, and privateKey.
37+
it('keeps the search key out of connections, references, and plain text', () => {
38+
expect(searchApiKeyField?.password).toBe(true)
39+
expect(searchApiKeyField?.paramVisibility).toBe('user-only')
40+
expect(searchApiKeyField?.connectionDroppable).toBe(false)
41+
})
42+
43+
it('declares the key as dependent on the provider, which is what clears it in the editor', () => {
44+
expect(searchApiKeyField?.dependsOn).toEqual(['searchProvider'])
45+
})
46+
47+
it('shows the key field only once a provider is selected', () => {
48+
expect(searchKeyVisible({ searchProvider: 'exa' })).toBe(true)
49+
expect(searchKeyVisible({ searchProvider: 'firecrawl' })).toBe(true)
50+
expect(searchKeyVisible({ searchProvider: 'none' })).toBe(false)
51+
expect(searchKeyVisible({ searchProvider: '' })).toBe(false)
52+
})
53+
54+
// A Pi block saved before this field existed has no stored value, and the serializer does not
55+
// inject subBlock defaults — so `undefined` has to behave like None here too.
56+
it('hides the key field on blocks saved before the field existed', () => {
57+
expect(searchKeyVisible({})).toBe(false)
58+
expect(searchKeyVisible({ searchProvider: undefined })).toBe(false)
59+
})
60+
61+
// The field is the only source for the search key — no workspace BYOK fallback, no hosted key —
62+
// so it has to be required. Safe despite the condition: the serializer's required check returns
63+
// early for fields that are not visible, which is what keeps a Pi block with search off from
64+
// failing validation over a key it does not need.
65+
it('requires the key, which the hidden case must not enforce', () => {
66+
expect(searchApiKeyField?.required).toBe(true)
67+
expect(searchKeyVisible({ searchProvider: 'none' })).toBe(false)
68+
expect(searchKeyVisible({})).toBe(false)
69+
})
70+
71+
// `inputs` is the block's type map, not the delivery mechanism — the handler reads resolved
72+
// params — but an undeclared input is a convention break the next block author would copy.
73+
it('declares both fields in the block input map', () => {
74+
expect(PiBlock.inputs.searchProvider).toBeDefined()
75+
expect(PiBlock.inputs.searchApiKey).toBeDefined()
76+
})
77+
})

apps/sim/blocks/blocks/pi.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,45 @@ const AUTHORING_MODES: { field: 'mode'; value: Array<'cloud' | 'local'> } = {
5353
}
5454
const MEMORY_TYPES = ['conversation', 'sliding_window', 'sliding_window_tokens']
5555

56+
const SEARCH_PROVIDER_OPTIONS = [
57+
{ label: 'None', id: 'none' },
58+
{ label: 'Exa', id: 'exa' },
59+
{ label: 'Serper', id: 'serper' },
60+
{ label: 'Parallel AI', id: 'parallel' },
61+
{ label: 'Firecrawl', id: 'firecrawl' },
62+
]
63+
64+
/**
65+
* Mirrors `getApiKeyCondition()` for the model key: the search key's visibility is computed rather
66+
* than declarative. The never-matching sentinel is how `buildModelVisibilityCondition` hides the
67+
* model key when nothing is selected, and it is what keeps the field hidden on a block saved before
68+
* this field existed — such a block has no stored `searchProvider`, and the declarative negative
69+
* form would show the field, because a scalar `not` condition evaluates `undefined !== 'none'` as
70+
* true.
71+
*/
72+
function getSearchApiKeyCondition() {
73+
return (values?: Record<string, unknown>) => {
74+
const provider = typeof values?.searchProvider === 'string' ? values.searchProvider : ''
75+
if (!provider || provider === 'none') {
76+
return { field: 'searchProvider', value: '__no_search_provider__' }
77+
}
78+
return { field: 'searchProvider', value: provider }
79+
}
80+
}
81+
5682
export const PiBlock: BlockConfig<PiResponse> = {
5783
type: 'pi',
5884
name: 'Pi Coding Agent',
5985
description: 'Run an autonomous coding agent on a repo',
6086
authMode: AuthMode.ApiKey,
6187
longDescription:
62-
'The Pi Coding Agent runs the Pi harness against a real repository. Create PR spins up an isolated sandbox, clones a GitHub repo, edits with native shell + git, and opens a pull request. Review Code checks out a pinned PR snapshot with read-only tools and posts a structured review with optional inline comments. Local Dev edits files on your own machine over SSH. Create PR and Local Dev can reuse skills and multi-turn memory; Review Code runs without either because PR contents are untrusted.',
88+
'The Pi Coding Agent runs the Pi harness against a real repository. Create PR spins up an isolated sandbox, clones a GitHub repo, edits with native shell + git, and opens a pull request. Review Code checks out a pinned PR snapshot with read-only tools and posts a structured review with optional inline comments. Local Dev edits files on your own machine over SSH. Create PR and Local Dev can reuse skills and multi-turn memory; Review Code runs without either because PR contents are untrusted. Any mode can optionally get one web_search tool backed by your own Exa, Serper, Parallel AI, or Firecrawl key; the agent writes its own queries, so repository content may reach the provider, and results are untrusted third-party data.',
6389
bestPractices: `
6490
- Use Create PR for hands-off changes against a GitHub repo where a reviewable PR is the deliverable.
6591
- Use Review Code to analyze an existing PR and leave summary + inline review comments.
6692
- Use Local Dev to edit a repo on your own machine; expose the machine on a public hostname/tunnel so Sim can reach it over SSH.
6793
- Create PR requires your own provider API key because the model runs in the sandbox. Review Code keeps the model key in Sim and can use either BYOK or a hosted key.
94+
- Internet Search is off by default and always needs your own key for the selected provider, entered on the block. There is no workspace BYOK fallback and no hosted key. Leave it on None unless the task genuinely needs external information.
6895
`,
6996
category: 'blocks',
7097
integrationType: IntegrationType.AI,
@@ -122,6 +149,37 @@ export const PiBlock: BlockConfig<PiResponse> = {
122149

123150
...getProviderCredentialSubBlocks(),
124151

152+
{
153+
id: 'searchProvider',
154+
title: 'Internet Search',
155+
type: 'dropdown',
156+
defaultValue: 'none',
157+
options: SEARCH_PROVIDER_OPTIONS,
158+
tooltip:
159+
'Gives the agent a single web_search tool backed by the selected provider. Search always uses your own key for that provider, never a Sim-hosted one, because Create PR places the key inside the coding sandbox.',
160+
},
161+
{
162+
id: 'searchApiKey',
163+
title: 'Search API Key',
164+
type: 'short-input',
165+
password: true,
166+
paramVisibility: 'user-only',
167+
connectionDroppable: false,
168+
placeholder: 'Your key for the selected provider',
169+
// The only source: search has no workspace BYOK fallback and never uses a Sim-hosted key, and
170+
// unlike the model key this field is shown on every deployment. Marking it required is what
171+
// surfaces that in the editor rather than at the start of a run.
172+
required: true,
173+
// Scoped to the editor on purpose: the clear-on-switch is driven by `dependsOn` through the
174+
// collaborative setter, so a workflow imported, forked, or updated through the API keeps
175+
// whatever key was stored. Promising an unconditional clear would be wrong in exactly the
176+
// case where sending the previous provider's key to a new vendor actually matters.
177+
tooltip:
178+
'Key for the selected search provider. Changing the provider in the editor clears this field, so re-enter the key for the one you picked. Imported or API-updated workflows keep the saved key — check it belongs to the selected provider.',
179+
condition: getSearchApiKeyCondition(),
180+
dependsOn: ['searchProvider'],
181+
},
182+
125183
{
126184
id: 'owner',
127185
title: 'Repository Owner',
@@ -434,6 +492,11 @@ export const PiBlock: BlockConfig<PiResponse> = {
434492
conversationId: { type: 'string', description: 'Conversation ID for memory' },
435493
slidingWindowSize: { type: 'string', description: 'Number of messages for sliding window' },
436494
slidingWindowTokens: { type: 'string', description: 'Max tokens for token-based window' },
495+
searchProvider: {
496+
type: 'string',
497+
description: 'Web search provider for the agent: none, exa, serper, parallel, or firecrawl',
498+
},
499+
searchApiKey: { type: 'string', description: 'API key for the selected search provider' },
437500
...PROVIDER_CREDENTIAL_INPUTS,
438501
},
439502
outputs: {

apps/sim/executor/handlers/pi/backend.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { TSchema } from 'typebox'
1111
import type { SSHConnectionConfig } from '@/app/api/tools/ssh/utils'
1212
import type { Message } from '@/executor/handlers/agent/types'
1313
import type { PiEvent, PiRunTotals } from '@/executor/handlers/pi/events'
14+
import type { PiSearchProvider } from '@/executor/handlers/pi/keys'
1415
import type { PiSupportedProvider } from '@/providers/pi-provider-configs'
1516

1617
/** A conversation message seeded into the Pi run (subset of the Agent block's message). */
@@ -31,6 +32,7 @@ export type PiSshConnection = Pick<
3132
/** Result of invoking a tool Pi called. */
3233
export interface PiToolResult {
3334
text: string
35+
/** Reported to Pi by throwing `text` from the converted tool; see `toPiTool` for why. */
3436
isError: boolean
3537
}
3638

@@ -43,9 +45,27 @@ export interface PiToolSpec {
4345
name: string
4446
description: string
4547
parameters: TSchema
48+
/**
49+
* Guideline bullets Pi folds into the system prompt while the tool is active. This is the
50+
* trusted channel: a `description` travels in the provider request's tool-definition array, so
51+
* guidance placed there carries the same trust level as the payload it describes. Dropped in
52+
* Review Code, which supplies a sealed `customPrompt` instead.
53+
*/
54+
promptGuidelines?: string[]
4655
execute: (args: Record<string, unknown>) => Promise<PiToolResult>
4756
}
4857

58+
/** Optional web search for a Pi run, resolved by the handler before mode dispatch. */
59+
export interface PiSearchConfig {
60+
provider: PiSearchProvider
61+
apiKey: string
62+
/**
63+
* Host-side tool for the two SDK modes. Absent for `cloud`, which has no host in the loop and
64+
* registers a sandbox extension instead, so a spec built there could never execute.
65+
*/
66+
tool?: PiToolSpec
67+
}
68+
4969
interface PiRunBaseParams {
5070
/** Sim's catalog ID, retained for billing and output. */
5171
model: string
@@ -56,6 +76,7 @@ interface PiRunBaseParams {
5676
isBYOK: boolean
5777
task: string
5878
thinkingLevel?: string
79+
search?: PiSearchConfig
5980
}
6081

6182
interface PiContextualRunParams extends PiRunBaseParams {

0 commit comments

Comments
 (0)