Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 52 additions & 24 deletions apps/docs/content/docs/en/workflows/blocks/pi.mdx

Large diffs are not rendered by default.

104 changes: 81 additions & 23 deletions apps/sim/blocks/blocks/pi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface PiResponse extends ToolResponse {
diff?: string
prUrl?: string
branch?: string
reviewUrl?: string
commentsPosted?: number
tokens?: {
input?: number
output?: number
Expand All @@ -36,6 +38,14 @@ interface PiResponse extends ToolResponse {
}

const CLOUD: { field: 'mode'; value: 'cloud' } = { field: 'mode', value: 'cloud' }
const CLOUD_REVIEW: { field: 'mode'; value: 'cloud_review' } = {
field: 'mode',
value: 'cloud_review',
}
const CLOUD_ANY: { field: 'mode'; value: Array<'cloud' | 'cloud_review'> } = {
field: 'mode',
value: ['cloud', 'cloud_review'],
}
const LOCAL: { field: 'mode'; value: 'local' } = { field: 'mode', value: 'local' }
const MEMORY_TYPES = ['conversation', 'sliding_window', 'sliding_window_tokens']

Expand All @@ -45,11 +55,12 @@ export const PiBlock: BlockConfig<PiResponse> = {
description: 'Run an autonomous coding agent on a repo',
authMode: AuthMode.ApiKey,
longDescription:
'The Pi Coding Agent runs the Pi harness against a real repository. In Cloud mode it spins up an isolated sandbox, clones a connected GitHub repo, edits and tests with native shell + git, and opens a pull request. In Local mode it edits files on your own machine over SSH. Both modes stream progress and reuse your models, skills, and multi-turn memory.',
'The Pi Coding Agent runs the Pi harness against a real repository. Cloud PR spins up an isolated sandbox, clones a GitHub repo, edits with native shell + git, and opens a pull request. Cloud Code Review checks out an existing PR and posts a structured review with optional inline comments. Local mode edits files on your own machine over SSH. All modes stream progress and reuse your models, skills, and multi-turn memory.',
bestPractices: `
- Use Cloud mode for hands-off changes against a GitHub repo where a reviewable PR is the deliverable.
- Use Cloud PR for hands-off changes against a GitHub repo where a reviewable PR is the deliverable.
- Use Cloud Code Review to analyze an existing PR and leave summary + inline review comments.
- Use Local mode to edit a repo on your own machine; expose the machine on a public hostname/tunnel so Sim can reach it over SSH.
- Cloud mode requires your own provider API key (BYOK); the model key is never injected as a hosted key into the sandbox.
- Cloud modes require your own provider API key (BYOK); the model key is never injected as a hosted key into the sandbox.
`,
category: 'blocks',
integrationType: IntegrationType.AI,
Expand All @@ -60,7 +71,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
id: 'mode',
title: 'Mode',
type: 'dropdown',
// Cloud mode runs in an E2B sandbox; only offer it where E2B is enabled.
// Cloud modes run in an E2B sandbox; only offer them where E2B is enabled.
value: () => (isTruthy(getEnv('NEXT_PUBLIC_E2B_ENABLED')) ? 'cloud' : 'local'),
options: () => {
const options = [
Expand All @@ -71,11 +82,18 @@ export const PiBlock: BlockConfig<PiResponse> = {
},
]
if (isTruthy(getEnv('NEXT_PUBLIC_E2B_ENABLED'))) {
options.unshift({
label: 'Cloud',
id: 'cloud',
description: 'Runs in an isolated sandbox, clones your repo, and opens a PR',
})
options.unshift(
{
label: 'Cloud PR',
id: 'cloud',
description: 'Runs in an isolated sandbox, clones your repo, and opens a PR',
},
{
label: 'Cloud Code Review',
id: 'cloud_review',
description: 'Reviews an existing PR and posts GitHub review comments',
}
)
}
return options
},
Expand Down Expand Up @@ -106,26 +124,27 @@ export const PiBlock: BlockConfig<PiResponse> = {
type: 'short-input',
placeholder: 'e.g., your-org',
required: true,
condition: CLOUD,
condition: CLOUD_ANY,
},
{
id: 'repo',
title: 'Repository Name',
type: 'short-input',
placeholder: 'e.g., my-repo',
required: true,
condition: CLOUD,
condition: CLOUD_ANY,
},
{
id: 'githubToken',
title: 'GitHub Token',
type: 'short-input',
password: true,
paramVisibility: 'user-only',
placeholder: 'GitHub personal access token (repo scope)',
tooltip: 'Personal access token with repo scope, used to clone, push, and open the PR.',
placeholder: 'GitHub personal access token',
tooltip:
'Personal access token used for GitHub access. Cloud PR needs clone/push/PR permissions; Cloud Code Review needs clone + review permissions.',
required: true,
condition: CLOUD,
condition: CLOUD_ANY,
},
{
id: 'baseBranch',
Expand Down Expand Up @@ -167,6 +186,27 @@ export const PiBlock: BlockConfig<PiResponse> = {
mode: 'advanced',
condition: CLOUD,
},
{
id: 'pullNumber',
title: 'Pull Request Number',
type: 'short-input',
placeholder: 'e.g., 42',
required: true,
condition: CLOUD_REVIEW,
},
{
id: 'reviewEvent',
title: 'Review Event',
type: 'dropdown',
defaultValue: 'COMMENT',
options: [
{ label: 'Comment', id: 'COMMENT' },
{ label: 'Request changes', id: 'REQUEST_CHANGES' },
{ label: 'Approve', id: 'APPROVE' },
],
tooltip: 'GitHub review action submitted with the agent findings.',
condition: CLOUD_REVIEW,
},

{
id: 'host',
Expand Down Expand Up @@ -336,17 +376,25 @@ export const PiBlock: BlockConfig<PiResponse> = {
access: [],
},
inputs: {
mode: { type: 'string', description: 'Execution mode: cloud or local' },
mode: {
type: 'string',
description: 'Execution mode: cloud, cloud_review, or local',
},
task: { type: 'string', description: 'Instruction for the coding agent' },
model: { type: 'string', description: 'AI model to use' },
owner: { type: 'string', description: 'GitHub repository owner (cloud mode)' },
repo: { type: 'string', description: 'GitHub repository name (cloud mode)' },
githubToken: { type: 'string', description: 'GitHub token override (cloud mode)' },
baseBranch: { type: 'string', description: 'Base branch for the PR (cloud mode)' },
branchName: { type: 'string', description: 'Branch to create (cloud mode)' },
draft: { type: 'boolean', description: 'Open the PR as a draft (cloud mode)' },
prTitle: { type: 'string', description: 'Pull request title (cloud mode)' },
prBody: { type: 'string', description: 'Pull request body (cloud mode)' },
owner: { type: 'string', description: 'GitHub repository owner (cloud modes)' },
repo: { type: 'string', description: 'GitHub repository name (cloud modes)' },
githubToken: { type: 'string', description: 'GitHub token (cloud modes)' },
baseBranch: { type: 'string', description: 'Base branch for the PR (Cloud PR)' },
branchName: { type: 'string', description: 'Branch to create (Cloud PR)' },
draft: { type: 'boolean', description: 'Open the PR as a draft (Cloud PR)' },
prTitle: { type: 'string', description: 'Pull request title (Cloud PR)' },
prBody: { type: 'string', description: 'Pull request body (Cloud PR)' },
pullNumber: { type: 'number', description: 'Pull request number (Cloud Code Review)' },
reviewEvent: {
type: 'string',
description: 'GitHub review event: COMMENT, REQUEST_CHANGES, or APPROVE',
},
host: { type: 'string', description: 'SSH host (local mode)' },
port: { type: 'number', description: 'SSH port (local mode)' },
username: { type: 'string', description: 'SSH username (local mode)' },
Expand Down Expand Up @@ -379,6 +427,16 @@ export const PiBlock: BlockConfig<PiResponse> = {
description: 'Branch pushed with the changes',
condition: CLOUD,
},
reviewUrl: {
type: 'string',
description: 'URL of the submitted GitHub review',
condition: CLOUD_REVIEW,
},
commentsPosted: {
type: 'number',
description: 'Number of inline review comments posted',
condition: CLOUD_REVIEW,
},
tokens: { type: 'json', description: 'Token usage statistics' },
cost: { type: 'json', description: 'Cost of the run' },
providerTiming: { type: 'json', description: 'Provider timing information' },
Expand Down
16 changes: 14 additions & 2 deletions apps/sim/executor/handlers/pi/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export interface PiLocalRunParams extends PiRunBaseParams {
tools: PiToolSpec[]
}

/** Parameters for a cloud (E2B) Pi run. */
/** Parameters for a cloud (E2B) Pi run that opens a PR. */
export interface PiCloudRunParams extends PiRunBaseParams {
mode: 'cloud'
owner: string
Expand All @@ -75,7 +75,17 @@ export interface PiCloudRunParams extends PiRunBaseParams {
prBody?: string
}

export type PiRunParams = PiLocalRunParams | PiCloudRunParams
/** Parameters for a cloud (E2B) Pi run that reviews an existing PR. */
export interface PiCloudReviewRunParams extends PiRunBaseParams {
mode: 'cloud_review'
owner: string
repo: string
githubToken: string
pullNumber: number
reviewEvent: 'COMMENT' | 'REQUEST_CHANGES' | 'APPROVE'
}

export type PiRunParams = PiLocalRunParams | PiCloudRunParams | PiCloudReviewRunParams

/** Progress callbacks and cancellation passed into a backend run. */
export interface PiRunContext {
Expand All @@ -90,6 +100,8 @@ export interface PiRunResult {
diff?: string
prUrl?: string
branch?: string
reviewUrl?: string
commentsPosted?: number
}

/** A Pi execution backend. Implemented by the local (SSH) and cloud (E2B) runners. */
Expand Down
60 changes: 11 additions & 49 deletions apps/sim/executor/handlers/pi/cloud-backend.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Cloud-mode backend: runs the Pi CLI inside an E2B sandbox against a cloned
* Cloud PR backend: runs the Pi CLI inside an E2B sandbox against a cloned
* GitHub repo, then pushes a branch and opens a PR. Secrets are isolated per
* command (S2/KTD10): the GitHub token is present only for the clone and push
* commands (and stripped from the cloned remote), while the Pi loop runs with a
Expand All @@ -15,9 +15,18 @@
import { createLogger } from '@sim/logger'
import { generateShortId } from '@sim/utils/id'
import { truncate } from '@sim/utils/string'
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
import { withPiSandbox } from '@/lib/execution/e2b'
import type { PiBackendRun, PiCloudRunParams } from '@/executor/handlers/pi/backend'
import {
CLONE_TIMEOUT_MS,
extractMarkerValues,
PI_SCRIPT,
PI_TIMEOUT_MS,
PROMPT_PATH,
REPO_DIR,
raceAbort,
scrubGitSecrets,
} from '@/executor/handlers/pi/cloud-shared'
import { buildPiPrompt } from '@/executor/handlers/pi/context'
import {
applyPiEvent,
Expand All @@ -30,16 +39,9 @@ import { executeTool } from '@/tools'

const logger = createLogger('PiCloudBackend')

const REPO_DIR = '/workspace/repo'
const DIFF_PATH = '/workspace/pi.diff'

const PROMPT_PATH = '/workspace/pi-prompt.txt'
const COMMIT_MSG_PATH = '/workspace/pi-commit.txt'

const PUSH_ERR_PATH = '/workspace/pi-push-err.txt'
const CLONE_TIMEOUT_MS = 10 * 60 * 1000

const PI_TIMEOUT_MS = getMaxExecutionTimeout()
const FINALIZE_TIMEOUT_MS = 10 * 60 * 1000
const MAX_DIFF_BYTES = 200_000
const COMMIT_TITLE_MAX = 72
Expand Down Expand Up @@ -68,9 +70,6 @@ echo "__DEFAULT_BRANCH__=$DEFAULT_BRANCH"
git checkout -b "$BRANCH"
git remote set-url origin "https://github.com/$REPO_OWNER/$REPO_NAME.git"`

const PI_SCRIPT = `cd ${REPO_DIR}
pi -p --mode json --provider "$PI_PROVIDER" --model "$PI_MODEL" --thinking "$PI_THINKING" < ${PROMPT_PATH}`

// Finalize is split so the GitHub token is in scope for ONLY the push. `git add`,
// `commit`, and `diff` run repo-config-driven programs that `core.hooksPath` does
// NOT disable — gitattributes clean/smudge filters (on add), `core.fsmonitor`
Expand All @@ -95,43 +94,6 @@ if git diff --quiet "$BASE_SHA" HEAD; then echo "__NO_CHANGES__=1"; else echo "_
const PUSH_SCRIPT = `cd ${REPO_DIR}
git -c core.hooksPath=/dev/null -c credential.helper= -c core.fsmonitor= push "https://x-access-token:$GITHUB_TOKEN@github.com/$REPO_OWNER/$REPO_NAME.git" "$BRANCH" >/dev/null 2>${PUSH_ERR_PATH} && echo "__PUSHED__=1"`

function raceAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
if (!signal) return promise
if (signal.aborted) return Promise.reject(new Error('Pi run aborted'))
return new Promise<T>((resolve, reject) => {
const onAbort = () => reject(new Error('Pi run aborted'))
signal.addEventListener('abort', onAbort, { once: true })
promise.then(
(value) => {
signal.removeEventListener('abort', onAbort)
resolve(value)
},
(error) => {
signal.removeEventListener('abort', onAbort)
reject(error)
}
)
})
}

function extractMarkerValues(stdout: string, prefix: string): string[] {
return stdout
.split('\n')
.filter((line) => line.startsWith(prefix))
.map((line) => line.slice(prefix.length).trim())
.filter(Boolean)
}

/**
* Redacts the GitHub token from git output before it is surfaced in an error.
* Removes the literal token and any URL userinfo (`//user:token@`), so a failure
* message can quote git's real stderr without leaking the credential.
*/
function scrubGitSecrets(text: string, token: string): string {
const withoutToken = token ? text.split(token).join('***') : text
return withoutToken.replace(/\/\/[^/@\s]+@/g, '//***@')
}

function buildPrBody(task: string, finalText: string): string {
const summary = finalText.trim()
? truncate(finalText.trim(), PR_SUMMARY_MAX)
Expand Down
Loading
Loading