Skip to content

Commit 835ba61

Browse files
committed
feat(pi): add pi coding agent harness
1 parent e96b150 commit 835ba61

34 files changed

Lines changed: 3375 additions & 16 deletions

File tree

apps/docs/content/docs/en/workflows/blocks/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"title": "Core Blocks",
33
"pages": [
44
"agent",
5+
"pi",
56
"api",
67
"function",
78
"condition",
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
title: Pi Coding Agent
3+
description: The Pi Coding Agent block runs an autonomous coding agent on a real repository — in an isolated cloud sandbox that opens a pull request, or on your own machine over SSH.
4+
pageType: reference
5+
---
6+
7+
import { BlockPreview } from '@/components/workflow-preview'
8+
import { FAQ } from '@/components/ui/faq'
9+
10+
The **Pi Coding Agent block** runs the [Pi](https://github.com/earendil-works/pi-mono) coding harness against a real repository. You give it a task and a model; it reads, edits, and runs files, then either opens a pull request or changes your files in place. It reuses your models, [skills](/agents/skills), and multi-turn [memory](#memory), and streams its progress as it works.
11+
12+
It has two modes that decide *where* it runs and *how* its changes land:
13+
14+
- **Cloud** — spins up an isolated sandbox, clones a connected GitHub repo, edits and tests with native shell + git, and opens a **pull request**.
15+
- **Local** — connects to your own machine over **SSH** and edits files there directly.
16+
17+
<BlockPreview type="pi" />
18+
19+
## Modes
20+
21+
Pick the mode with the **Mode** dropdown. The fields below it change to match.
22+
23+
### Cloud
24+
25+
Cloud runs entirely inside a disposable sandbox, so it never touches your machine. It clones the repo, lets the agent work with full read/shell/edit/git, pushes a branch, and opens a PR you review and merge.
26+
27+
- Requires sandbox execution to be enabled (the Cloud option only appears when it is).
28+
- Requires **your own provider API key (BYOK)** — the model key is handed to the sandbox, so Sim never injects a hosted key there.
29+
- Needs a **GitHub token** with permission to clone, push, and open a PR (see [Setup](#setup-cloud)).
30+
- The deliverable is a **pull request** — nothing is committed to your default branch directly.
31+
32+
### Local
33+
34+
Local runs the agent against a repository on a machine you control, reached over SSH. Changes are written **in place** — there's no PR; you review them as normal git changes on that machine.
35+
36+
- The machine must be reachable on a **public hostname**`localhost` and LAN/private addresses are blocked. Expose it with a tunnel (see [Setup](#setup-local)).
37+
- The agent's file and shell tools are confined to the **Repository Path** you configure.
38+
- You can also expose **Sim tools** (Gmail, Slack, Exa, …) to the agent so it can act beyond the repo while it works.
39+
40+
## Configuration
41+
42+
### Task
43+
44+
What the agent should do, in plain language — for example *"Add input validation to the signup form and a test for it."* Insert a [connection tag](/workflows/connections) to pass an earlier output, like `<start.input>`.
45+
46+
### Model
47+
48+
The model that drives the agent. Defaults to `claude-sonnet-4-6`. The dropdown lists only models the Pi harness can run: **OpenAI, Anthropic, Google (Gemini), xAI, DeepSeek, Mistral, Groq, Cerebras, and OpenRouter**.
49+
50+
### API Key
51+
52+
Your key for the chosen provider. On hosted Sim it's optional for Local runs (a hosted key is used and metered to your workspace), but **Cloud always requires your own key** — enter it in this field. For OpenAI, Anthropic, Google, and Mistral you can instead store a workspace key in **Settings → BYOK**; other providers must use this field.
53+
54+
### Repository (Cloud)
55+
56+
- **Repository Owner / Repository Name** — the GitHub repo to clone and open the PR against (for example `your-org` / `your-repo`).
57+
- **GitHub Token** — a personal access token used to clone, push, and open the PR. See [Setup](#setup-cloud) for the exact permissions.
58+
- **Base Branch** — the branch the PR is opened against and cloned from. Defaults to the repository's default branch.
59+
- **Branch Name** *(advanced)* — the branch to push. Auto-generated when blank.
60+
- **Open as Draft PR** *(advanced)* — opens the PR as a draft. On by default.
61+
- **PR Title / PR Body** *(advanced)* — generated from the run when blank.
62+
63+
### Connection (Local)
64+
65+
- **Host** — the public hostname or tunnel for the target machine (for example `2.tcp.ngrok.io`). Not `localhost` or a LAN address.
66+
- **Username** — the SSH user (for example `ubuntu`, `root`, or your macOS account).
67+
- **Authentication Method**`Password` or `Private Key`.
68+
- **Password / Private Key** — the credential for that method. Use a key where you can.
69+
- **Repository Path** — the absolute path to the repo on the target machine (for example `/home/user/my-repo`). The agent's tools are confined to this directory.
70+
- **Port** *(advanced)* — the SSH port. Defaults to `22`; set this to your tunnel's port if it differs.
71+
- **Passphrase** *(advanced)* — for an encrypted private key.
72+
73+
### Tools (Local)
74+
75+
Sim tools the agent can call while it works — search a knowledge base, send a Slack message, call any of the [integrations](/integrations). They run through Sim with your connected credentials, exactly like the [Agent block](/workflows/blocks/agent). MCP and custom tools aren't supported here yet (they appear greyed out).
76+
77+
### Skills
78+
79+
[Agent skills](/agents/skills) the agent can use — reusable instruction packages like a coding standard or a review playbook. They're shared with the Agent block, so a skill you author once works in both.
80+
81+
### Thinking Level
82+
83+
For models with extended reasoning, how much the model thinks before acting. Higher is more thorough but slower and costs more tokens. Defaults to `medium`.
84+
85+
### Memory
86+
87+
Multi-turn memory keyed by a conversation ID, shared with the [Agent block](/workflows/blocks/agent):
88+
89+
- **None.** Each run is independent.
90+
- **Conversation.** The full history for that conversation ID.
91+
- **Sliding window (messages).** The most recent N messages.
92+
- **Sliding window (tokens).** Recent messages up to a token budget.
93+
94+
Reuse the same **Conversation ID** across runs to continue a thread. Each turn stores your task and the agent's final summary, which are folded into the next run's prompt.
95+
96+
## Outputs
97+
98+
| Output | What it is |
99+
| --- | --- |
100+
| `<pi.content>` | The agent's final message / run summary |
101+
| `<pi.changedFiles>` | The files the agent changed |
102+
| `<pi.diff>` | A unified diff of the changes |
103+
| `<pi.prUrl>` | URL of the opened pull request *(Cloud)* |
104+
| `<pi.branch>` | The branch pushed with the changes *(Cloud)* |
105+
| `<pi.model>` | The model that ran |
106+
| `<pi.tokens>` | Token usage, an object `{ input, output, total }` |
107+
| `<pi.cost>` | Estimated cost of the run |
108+
| `<pi.providerTiming>` | Timing, an object `{ startTime, endTime, duration }` |
109+
110+
## Setup
111+
112+
### Cloud
113+
114+
Cloud runs in a sandbox image with the Pi CLI and git baked in.
115+
116+
1. **Enable sandbox execution.** On self-hosted Sim, set `E2B_ENABLED=true`, `E2B_API_KEY`, `E2B_PI_TEMPLATE_ID` (the Pi template id), and `NEXT_PUBLIC_E2B_ENABLED=true` (this reveals the Cloud option in the UI). Build the template with `bun run apps/sim/scripts/build-pi-e2b-template.ts`. The Cloud option stays hidden until `NEXT_PUBLIC_E2B_ENABLED` is set.
117+
2. **Bring your own model key.** Set the provider API key in the block's API Key field (or, for OpenAI/Anthropic/Google/Mistral, in **Settings → BYOK**).
118+
3. **Create a GitHub token** with permission to clone, push, and open a PR:
119+
- *Fine-grained:* select the repo, then **Contents: Read and write** + **Pull requests: Read and write**.
120+
- *Classic:* the **`repo`** scope. For org repos, authorize the token for SSO.
121+
122+
### Local
123+
124+
1. **Enable SSH** on the target machine (on macOS: System Settings → General → Sharing → Remote Login).
125+
2. **Expose it on a public host.** Sim blocks `localhost`/LAN, so use a TCP tunnel — for example `ngrok tcp 22`, which gives a `host:port` to put in **Host** and **Port**.
126+
3. **Use a model your provider supports** (for example a Claude model with an Anthropic key). Set the credential method and **Repository Path**, then run.
127+
128+
## Best Practices
129+
130+
- **Scope the task.** A specific instruction ("fix the failing `auth` test and add a regression case") produces far better results than a vague one.
131+
- **Use Cloud for hands-off PRs, Local for your working tree.** Cloud is safest for unattended changes (everything lands in a reviewable PR); Local is for iterating on a repo you already have checked out.
132+
- **Prefer key auth and tear down tunnels.** A public SSH tunnel is a real attack surface — use a private key and stop the tunnel when you're done.
133+
- **Reuse a Conversation ID for follow-ups.** It carries the prior task and outcome into the next run so the agent can build on its own work.
134+
135+
<FAQ items={[
136+
{ question: "What's the difference between Cloud and Local mode?", answer: "Cloud runs in a disposable sandbox, clones a GitHub repo, and opens a pull request — it never touches your machine. Local connects to your own machine over SSH and edits files in place (no PR). Cloud requires your own model key (BYOK); Local can use a hosted model key on hosted Sim." },
137+
{ question: "Which models can I use?", answer: "The model dropdown is filtered to providers the Pi harness can run with an API key: OpenAI, Anthropic, Google (Gemini), xAI, DeepSeek, Mistral, Groq, Cerebras, and OpenRouter. Providers that need richer config (Vertex, Bedrock, Azure) or a base URL (Ollama, vLLM, etc.) aren't offered." },
138+
{ question: "Why does Local mode 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." },
139+
{ question: "What GitHub permissions does Cloud mode 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." },
140+
{ question: "Can I give it Gmail, Slack, or other integrations?", answer: "Yes, in Local mode 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." },
141+
{ question: "Where do the changes go?", answer: "In Cloud mode, to a new branch and a pull request (read prUrl and branch). In Local mode, the files are edited in place on the target machine — review them with git there. Both modes also return changedFiles and a diff." },
142+
]} />

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,27 @@ function IconComponent({
454454
return <Icon className={className} />
455455
}
456456

457+
const UNSUPPORTED_CUSTOM_TOOL_MESSAGE = 'Custom tools are not supported by this block yet'
458+
const UNSUPPORTED_MCP_TOOL_MESSAGE = 'MCP tools are not supported by this block yet'
459+
460+
/**
461+
* Trailing "Unavailable" affordance for a tool category the consuming block
462+
* cannot execute. Rendered as the combobox item's suffix so the greyed-out row
463+
* still surfaces a tooltip explaining why on hover.
464+
*/
465+
function UnsupportedToolBadge({ message }: { message: string }) {
466+
return (
467+
<Tooltip.Root>
468+
<Tooltip.Trigger asChild>
469+
<span className='text-[var(--text-tertiary)] text-xs'>Unavailable</span>
470+
</Tooltip.Trigger>
471+
<Tooltip.Content>
472+
<span className='text-sm'>{message}</span>
473+
</Tooltip.Content>
474+
</Tooltip.Root>
475+
)
476+
}
477+
457478
export const ToolInput = memo(function ToolInput({
458479
blockId,
459480
subBlockId,
@@ -495,6 +516,16 @@ export const ToolInput = memo(function ToolInput({
495516
? (value as StoredTool[])
496517
: []
497518

519+
// Tool categories the consuming block can't run (declared on its tool-input
520+
// subBlock): shown in the picker but greyed out with a tooltip instead of added.
521+
const blockType = useWorkflowStore(useCallback((state) => state.blocks[blockId]?.type, [blockId]))
522+
const unsupportedToolTypes = useMemo<readonly ('mcp' | 'custom-tool')[]>(() => {
523+
const block = getAllBlocks().find((b) => b.type === blockType)
524+
return block?.subBlocks.find((sb) => sb.id === subBlockId)?.unsupportedToolTypes ?? []
525+
}, [blockType, subBlockId])
526+
const mcpUnsupported = unsupportedToolTypes.includes('mcp')
527+
const customUnsupported = unsupportedToolTypes.includes('custom-tool')
528+
498529
// Look up credential type for reactive condition filtering (e.g. service account detection).
499530
// Uses canonical resolution so the active field (basic vs advanced) is respected.
500531
const toolCredentialId = useMemo(() => {
@@ -1346,7 +1377,12 @@ export const ToolInput = memo(function ToolInput({
13461377
const groups: ComboboxOptionGroup[] = []
13471378

13481379
// MCP Server drill-down: when navigated into a server, show only its tools
1349-
if (mcpServerDrilldown && !permissionConfig.disableMcpTools && mcpToolsByServer.size > 0) {
1380+
if (
1381+
mcpServerDrilldown &&
1382+
!permissionConfig.disableMcpTools &&
1383+
!mcpUnsupported &&
1384+
mcpToolsByServer.size > 0
1385+
) {
13501386
const tools = mcpToolsByServer.get(mcpServerDrilldown)
13511387
if (tools && tools.length > 0) {
13521388
const server = mcpServers.find((s) => s.id === mcpServerDrilldown)
@@ -1458,7 +1494,10 @@ export const ToolInput = memo(function ToolInput({
14581494
setCustomToolModalOpen(true)
14591495
setOpen(false)
14601496
},
1461-
disabled: isPreview,
1497+
disabled: isPreview || customUnsupported,
1498+
suffixElement: customUnsupported ? (
1499+
<UnsupportedToolBadge message={UNSUPPORTED_CUSTOM_TOOL_MESSAGE} />
1500+
) : undefined,
14621501
})
14631502
}
14641503
if (!permissionConfig.disableMcpTools) {
@@ -1470,14 +1509,17 @@ export const ToolInput = memo(function ToolInput({
14701509
setOpen(false)
14711510
setMcpModalOpen(true)
14721511
},
1473-
disabled: isPreview,
1512+
disabled: isPreview || mcpUnsupported,
1513+
suffixElement: mcpUnsupported ? (
1514+
<UnsupportedToolBadge message={UNSUPPORTED_MCP_TOOL_MESSAGE} />
1515+
) : undefined,
14741516
})
14751517
}
14761518
if (actionItems.length > 0) {
14771519
groups.push({ items: actionItems })
14781520
}
14791521

1480-
if (!permissionConfig.disableCustomTools && customTools.length > 0) {
1522+
if (!permissionConfig.disableCustomTools && !customUnsupported && customTools.length > 0) {
14811523
groups.push({
14821524
section: 'Custom Tools',
14831525
items: customTools.map((customTool) => {
@@ -1507,7 +1549,7 @@ export const ToolInput = memo(function ToolInput({
15071549
}
15081550

15091551
// MCP Servers — root folder view
1510-
if (!permissionConfig.disableMcpTools && mcpToolsByServer.size > 0) {
1552+
if (!permissionConfig.disableMcpTools && !mcpUnsupported && mcpToolsByServer.size > 0) {
15111553
const serverItems: ComboboxOption[] = []
15121554

15131555
for (const [serverId, tools] of mcpToolsByServer) {
@@ -1620,6 +1662,8 @@ export const ToolInput = memo(function ToolInput({
16201662
handleSelectTool,
16211663
permissionConfig.disableCustomTools,
16221664
permissionConfig.disableMcpTools,
1665+
mcpUnsupported,
1666+
customUnsupported,
16231667
availableWorkflows,
16241668
isToolAlreadySelected,
16251669
])

0 commit comments

Comments
 (0)