Skip to content

Commit 8f7faa6

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
feat(pi): add plan mode
1 parent c212383 commit 8f7faa6

12 files changed

Lines changed: 609 additions & 45 deletions

File tree

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

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,20 @@ describe('Pi block search fields', () => {
8686
})
8787

8888
describe('Pi cloud authoring surface', () => {
89-
it('offers Create PR, Update PR, Review Code, and Local Dev as top-level modes', () => {
89+
it('offers Create PR, Update PR, Plan, Review Code, and Local Dev as top-level modes', () => {
9090
const mode = PiBlock.subBlocks.find((subBlock) => subBlock.id === 'mode')
9191
const options =
9292
typeof mode?.options === 'function'
9393
? mode.options()
9494
: (mode?.options as Array<{ id: string }> | undefined)
9595

96-
expect(options?.map(({ id }) => id)).toEqual(['cloud', 'cloud_branch', 'cloud_review', 'local'])
96+
expect(options?.map(({ id }) => id)).toEqual([
97+
'cloud',
98+
'cloud_branch',
99+
'cloud_plan',
100+
'cloud_review',
101+
'local',
102+
])
97103
})
98104

99105
it.each(['cloud', 'cloud_branch'])(
@@ -195,6 +201,64 @@ describe('Pi cloud authoring surface', () => {
195201
expect(evaluateSubBlockCondition(targetBranchField?.condition, { mode: 'local' })).toBe(false)
196202
})
197203

204+
it('shows only shared planning inputs in Plan mode', () => {
205+
for (const id of [
206+
'task',
207+
'model',
208+
'apiKey',
209+
'searchProvider',
210+
'owner',
211+
'repo',
212+
'githubToken',
213+
'baseBranch',
214+
'skills',
215+
'thinkingLevel',
216+
'memoryType',
217+
]) {
218+
const field = PiBlock.subBlocks.find((subBlock) => subBlock.id === id)
219+
expect(evaluateSubBlockCondition(field?.condition, { mode: 'cloud_plan' }), id).toBe(true)
220+
}
221+
222+
for (const id of [
223+
'targetBranch',
224+
'babysitMode',
225+
'reviewMentions',
226+
'branchName',
227+
'draft',
228+
'prState',
229+
'prTitle',
230+
'prBody',
231+
'pullNumber',
232+
'reviewEvent',
233+
'maxRounds',
234+
'host',
235+
'username',
236+
'authMethod',
237+
'password',
238+
'privateKey',
239+
'repoPath',
240+
'port',
241+
'passphrase',
242+
'tools',
243+
]) {
244+
const field = PiBlock.subBlocks.find((subBlock) => subBlock.id === id)
245+
expect(evaluateSubBlockCondition(field?.condition, { mode: 'cloud_plan' }), id).toBe(false)
246+
}
247+
248+
for (const id of ['changedFiles', 'diff', 'prUrl', 'branch', 'reviewUrl', 'commentsPosted']) {
249+
expect(
250+
evaluateSubBlockCondition(PiBlock.outputs[id]?.condition, { mode: 'cloud_plan' }),
251+
id
252+
).toBe(false)
253+
}
254+
for (const id of ['content', 'model', 'tokens', 'cost', 'providerTiming']) {
255+
expect(
256+
evaluateSubBlockCondition(PiBlock.outputs[id]?.condition, { mode: 'cloud_plan' }),
257+
id
258+
).toBe(true)
259+
}
260+
})
261+
198262
it('declares the target branch input and branch output for cloud authoring modes', () => {
199263
expect(PiBlock.inputs.targetBranch).toBeDefined()
200264
expect(

apps/sim/blocks/blocks/pi.ts

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,22 @@ const CLOUD_BRANCH: { field: 'mode'; value: 'cloud_branch' } = {
5656
}
5757
const CLOUD_ANY: {
5858
field: 'mode'
59-
value: Array<'cloud' | 'cloud_branch' | 'cloud_review'>
59+
value: Array<'cloud' | 'cloud_branch' | 'cloud_plan' | 'cloud_review'>
6060
} = {
6161
field: 'mode',
62-
value: ['cloud', 'cloud_branch', 'cloud_review'],
62+
value: ['cloud', 'cloud_branch', 'cloud_plan', 'cloud_review'],
6363
}
6464
const CLOUD_AUTHORING: { field: 'mode'; value: Array<'cloud' | 'cloud_branch'> } = {
6565
field: 'mode',
6666
value: ['cloud', 'cloud_branch'],
6767
}
68+
const CLOUD_SANDBOX: {
69+
field: 'mode'
70+
value: Array<'cloud' | 'cloud_branch' | 'cloud_plan'>
71+
} = {
72+
field: 'mode',
73+
value: ['cloud', 'cloud_branch', 'cloud_plan'],
74+
}
6875
const BABYSIT_ENABLED_VALUES: Array<true | 'true'> = [true, 'true']
6976
const CLOUD_WITH_BABYSIT: {
7077
field: 'mode'
@@ -106,12 +113,12 @@ function getCloudBranchWithoutBabysitCondition(values?: Record<string, unknown>)
106113
}
107114
}
108115
const LOCAL: { field: 'mode'; value: 'local' } = { field: 'mode', value: 'local' }
109-
const AUTHORING_MODES: {
116+
const CONTEXTUAL_MODES: {
110117
field: 'mode'
111-
value: Array<'cloud' | 'cloud_branch' | 'local'>
118+
value: Array<'cloud' | 'cloud_branch' | 'cloud_plan' | 'local'>
112119
} = {
113120
field: 'mode',
114-
value: ['cloud', 'cloud_branch', 'local'],
121+
value: ['cloud', 'cloud_branch', 'cloud_plan', 'local'],
115122
}
116123
const MEMORY_TYPES = ['conversation', 'sliding_window', 'sliding_window_tokens']
117124

@@ -146,29 +153,30 @@ const hostedModelApiKeyCondition = getApiKeyCondition()
146153
/**
147154
* API Key visibility for the Pi block.
148155
*
149-
* Create PR hands the model key to the sandbox as an environment variable, so
156+
* Plan, Create PR, and Update PR hand the model key to the sandbox as an environment variable, so
150157
* Sim never supplies a hosted key there — the field is shown for every model,
151158
* including ones that are hosted elsewhere in Sim. Review Code and Local Dev
152159
* keep the model client inside Sim, so they follow the standard hosted-model
153160
* rule and hide the field when Sim covers the key.
154161
*/
155162
const piApiKeyCondition = (values?: Record<string, unknown>) =>
156-
isPiByokOnlyMode(values?.mode) ? CLOUD_AUTHORING : hostedModelApiKeyCondition(values)
163+
isPiByokOnlyMode(values?.mode) ? CLOUD_SANDBOX : hostedModelApiKeyCondition(values)
157164

158165
export const PiBlock: BlockConfig<PiResponse> = {
159166
type: 'pi',
160167
name: 'Pi Coding Agent',
161168
description: 'Run an autonomous coding agent on a repo',
162169
authMode: AuthMode.ApiKey,
163170
longDescription:
164-
'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; Update PR checks out an existing remote branch, pushes commits back without force-pushing, and creates or updates its pull request. Babysit Mode then keeps the pull request under watch, fixing trusted bot review threads and failing required checks in bounded rounds. 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, Update 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.',
171+
'The Pi Coding Agent runs the Pi harness against a real repository. Plan explores a disposable sandbox checkout and returns an implementation plan without pushing changes. Create PR spins up an isolated sandbox, clones a GitHub repo, edits with native shell + git, and opens a pull request; Update PR checks out an existing remote branch, pushes commits back without force-pushing, and creates or updates its pull request. Babysit Mode then keeps the pull request under watch, fixing trusted bot review threads and failing required checks in bounded rounds. 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. Plan, Create PR, Update 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.',
165172
bestPractices: `
173+
- Use Plan to inspect a GitHub repo and produce an implementation plan without persisting changes.
166174
- Use Create PR for hands-off changes against a GitHub repo where a reviewable PR is the deliverable.
167175
- Use Update PR to continue work on an existing remote branch and create or update its pull request.
168176
- Enable Babysit Mode on Create PR or Update PR when trusted review bots and required checks should be monitored and fixed in bounded rounds.
169177
- Use Review Code to analyze an existing PR and leave summary + inline review comments.
170178
- 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.
171-
- Create PR and Update PR require your own provider API key for every model, including ones Sim hosts, because the model runs in the sandbox. Review Code and Local Dev keep the model key in Sim and can use either BYOK or a hosted key.
179+
- Plan, Create PR, and Update PR require your own provider API key for every model, including ones Sim hosts, because the model runs in the sandbox. Review Code and Local Dev keep the model key in Sim and can use either BYOK or a hosted key.
172180
- 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.
173181
`,
174182
category: 'blocks',
@@ -202,6 +210,11 @@ export const PiBlock: BlockConfig<PiResponse> = {
202210
id: 'cloud_branch',
203211
description: 'Updates an existing branch and creates or updates its pull request',
204212
},
213+
{
214+
label: 'Plan',
215+
id: 'cloud_plan',
216+
description: 'Explores a disposable checkout and returns an implementation plan',
217+
},
205218
{
206219
label: 'Review Code',
207220
id: 'cloud_review',
@@ -246,7 +259,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
246259
defaultValue: 'none',
247260
options: SEARCH_PROVIDER_OPTIONS,
248261
tooltip:
249-
'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 cloud authoring places the key inside the coding sandbox.',
262+
'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 sandbox modes place the key inside the coding sandbox.',
250263
},
251264
{
252265
id: 'searchApiKey',
@@ -294,7 +307,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
294307
paramVisibility: 'user-only',
295308
placeholder: 'GitHub personal access token',
296309
tooltip:
297-
'Personal access token used for GitHub access. Create PR and Update PR both need clone, push, and pull request read/write permissions. With Babysit Mode, either also needs check/Actions reads, thread writes, and issue comments. Review Code needs clone + review permissions.',
310+
'Personal access token used for GitHub access. Plan needs clone access only. Create PR and Update PR both need clone, push, and pull request read/write permissions. With Babysit Mode, either also needs check/Actions reads, thread writes, and issue comments. Review Code needs clone + review permissions.',
298311
required: true,
299312
condition: CLOUD_ANY,
300313
},
@@ -304,8 +317,8 @@ export const PiBlock: BlockConfig<PiResponse> = {
304317
type: 'short-input',
305318
placeholder: 'e.g., main (defaults to the repository default branch)',
306319
tooltip:
307-
'Create PR clones this branch and opens against it. Update PR changes an existing pull request only when set, or uses it when creating a missing pull request.',
308-
condition: CLOUD_AUTHORING,
320+
'Plan and Create PR clone this branch, defaulting to the repository default. Create PR opens against it. Update PR changes an existing pull request only when set, or uses it when creating a missing pull request.',
321+
condition: CLOUD_SANDBOX,
309322
},
310323
{
311324
id: 'targetBranch',
@@ -524,7 +537,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
524537
type: 'skill-input',
525538
defaultValue: [],
526539
mode: 'advanced',
527-
condition: AUTHORING_MODES,
540+
condition: CONTEXTUAL_MODES,
528541
},
529542
{
530543
id: 'thinkingLevel',
@@ -554,7 +567,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
554567
{ label: 'Sliding window (tokens)', id: 'sliding_window_tokens' },
555568
],
556569
mode: 'advanced',
557-
condition: AUTHORING_MODES,
570+
condition: CONTEXTUAL_MODES,
558571
},
559572
{
560573
id: 'conversationId',
@@ -564,12 +577,12 @@ export const PiBlock: BlockConfig<PiResponse> = {
564577
mode: 'advanced',
565578
required: {
566579
field: 'mode',
567-
value: ['cloud', 'cloud_branch', 'local'],
580+
value: ['cloud', 'cloud_branch', 'cloud_plan', 'local'],
568581
and: { field: 'memoryType', value: MEMORY_TYPES },
569582
},
570583
condition: {
571584
field: 'mode',
572-
value: ['cloud', 'cloud_branch', 'local'],
585+
value: ['cloud', 'cloud_branch', 'cloud_plan', 'local'],
573586
and: { field: 'memoryType', value: MEMORY_TYPES },
574587
},
575588
dependsOn: ['memoryType'],
@@ -582,7 +595,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
582595
mode: 'advanced',
583596
condition: {
584597
field: 'mode',
585-
value: ['cloud', 'cloud_branch', 'local'],
598+
value: ['cloud', 'cloud_branch', 'cloud_plan', 'local'],
586599
and: { field: 'memoryType', value: ['sliding_window'] },
587600
},
588601
dependsOn: ['memoryType'],
@@ -595,7 +608,7 @@ export const PiBlock: BlockConfig<PiResponse> = {
595608
mode: 'advanced',
596609
condition: {
597610
field: 'mode',
598-
value: ['cloud', 'cloud_branch', 'local'],
611+
value: ['cloud', 'cloud_branch', 'cloud_plan', 'local'],
599612
and: { field: 'memoryType', value: ['sliding_window_tokens'] },
600613
},
601614
dependsOn: ['memoryType'],
@@ -607,14 +620,14 @@ export const PiBlock: BlockConfig<PiResponse> = {
607620
inputs: {
608621
mode: {
609622
type: 'string',
610-
description: 'Execution mode: Create PR, Update PR, Review Code, or Local Dev',
623+
description: 'Execution mode: Plan, Create PR, Update PR, Review Code, or Local Dev',
611624
},
612625
task: { type: 'string', description: 'Instruction for the coding agent' },
613626
model: { type: 'string', description: 'AI model to use' },
614627
owner: { type: 'string', description: 'GitHub repository owner (cloud modes)' },
615628
repo: { type: 'string', description: 'GitHub repository name (cloud modes)' },
616629
githubToken: { type: 'string', description: 'GitHub token (cloud modes)' },
617-
baseBranch: { type: 'string', description: 'Base branch for the pull request' },
630+
baseBranch: { type: 'string', description: 'Branch to inspect or use as the PR base' },
618631
branchName: { type: 'string', description: 'Branch to create (Create PR)' },
619632
targetBranch: { type: 'string', description: 'Existing branch to update (Update PR)' },
620633
draft: { type: 'boolean', description: 'Open the PR as a draft (Create PR)' },
@@ -667,8 +680,16 @@ export const PiBlock: BlockConfig<PiResponse> = {
667680
outputs: {
668681
content: { type: 'string', description: 'Final agent message / run summary' },
669682
model: { type: 'string', description: 'Model used for the run' },
670-
changedFiles: { type: 'json', description: 'Files changed by the agent' },
671-
diff: { type: 'string', description: 'Unified diff of the changes' },
683+
changedFiles: {
684+
type: 'json',
685+
description: 'Files changed by the agent',
686+
condition: { field: 'mode', value: ['cloud', 'cloud_branch', 'cloud_review', 'local'] },
687+
},
688+
diff: {
689+
type: 'string',
690+
description: 'Unified diff of the changes',
691+
condition: { field: 'mode', value: ['cloud', 'cloud_branch', 'cloud_review', 'local'] },
692+
},
672693
prUrl: {
673694
type: 'string',
674695
description: 'URL of the created or babysat pull request',

apps/sim/blocks/pi-api-key-condition.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ describe('Pi API Key visibility', () => {
4242
expect(isApiKeyVisible({ mode: 'cloud', model: 'some-unhosted-model' })).toBe(true)
4343
})
4444

45+
it('shows the field in Plan even for a model Sim hosts', () => {
46+
expect(isApiKeyVisible({ mode: 'cloud_plan', model: hostedModel })).toBe(true)
47+
})
48+
4549
it.each([['local'], ['cloud_review']])(
4650
'hides the field in %s mode for a model Sim hosts',
4751
(mode) => {

0 commit comments

Comments
 (0)