Skip to content

Commit d382761

Browse files
BillLeoutsakosvl346Bill Leoutsakos
andauthored
feat(pi): add plan mode (#6372)
* chore(pi): organize mode implementations * feat(pi): add plan mode * fix(pi): preserve plan exploration timeout * fix(pi): clean plan mode output * fix(pi): stream only final plan content * chore: trigger CI after retargeting * fix(pi): compact plan mode event streams --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
1 parent 19c3171 commit d382761

18 files changed

Lines changed: 841 additions & 61 deletions

File tree

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

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,26 @@ 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+
])
103+
})
104+
105+
it('documents each mode label with its serialized ID', () => {
106+
expect(PiBlock.inputs.mode.description).toBe(
107+
'Execution mode: Plan (cloud_plan), Create PR (cloud), Update PR (cloud_branch), Review Code (cloud_review), or Local Dev (local)'
108+
)
97109
})
98110

99111
it.each(['cloud', 'cloud_branch'])(
@@ -195,6 +207,64 @@ describe('Pi cloud authoring surface', () => {
195207
expect(evaluateSubBlockCondition(targetBranchField?.condition, { mode: 'local' })).toBe(false)
196208
})
197209

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

apps/sim/blocks/blocks/pi.ts

Lines changed: 45 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,15 @@ 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:
624+
'Execution mode: Plan (cloud_plan), Create PR (cloud), Update PR (cloud_branch), Review Code (cloud_review), or Local Dev (local)',
611625
},
612626
task: { type: 'string', description: 'Instruction for the coding agent' },
613627
model: { type: 'string', description: 'AI model to use' },
614628
owner: { type: 'string', description: 'GitHub repository owner (cloud modes)' },
615629
repo: { type: 'string', description: 'GitHub repository name (cloud modes)' },
616630
githubToken: { type: 'string', description: 'GitHub token (cloud modes)' },
617-
baseBranch: { type: 'string', description: 'Base branch for the pull request' },
631+
baseBranch: { type: 'string', description: 'Branch to inspect or use as the PR base' },
618632
branchName: { type: 'string', description: 'Branch to create (Create PR)' },
619633
targetBranch: { type: 'string', description: 'Existing branch to update (Update PR)' },
620634
draft: { type: 'boolean', description: 'Open the PR as a draft (Create PR)' },
@@ -667,8 +681,16 @@ export const PiBlock: BlockConfig<PiResponse> = {
667681
outputs: {
668682
content: { type: 'string', description: 'Final agent message / run summary' },
669683
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' },
684+
changedFiles: {
685+
type: 'json',
686+
description: 'Files changed by the agent',
687+
condition: { field: 'mode', value: ['cloud', 'cloud_branch', 'cloud_review', 'local'] },
688+
},
689+
diff: {
690+
type: 'string',
691+
description: 'Unified diff of the changes',
692+
condition: { field: 'mode', value: ['cloud', 'cloud_branch', 'cloud_review', 'local'] },
693+
},
672694
prUrl: {
673695
type: 'string',
674696
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) => {

apps/sim/executor/handlers/pi/cloud/event-filter-source.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ function asString(value) {
4646
return typeof value === 'string' ? value : undefined
4747
}
4848
49+
function compactAssistantContent(value) {
50+
if (!Array.isArray(value)) return undefined
51+
const content = []
52+
for (const valueBlock of value) {
53+
const block = asRecord(valueBlock)
54+
if (block?.type === 'text' && typeof block.text === 'string') {
55+
content.push({ type: 'text', text: block.text })
56+
}
57+
}
58+
return content.length > 0 ? content : undefined
59+
}
60+
4961
function compactUsage(value) {
5062
const usage = asRecord(value)
5163
if (!usage) return null
@@ -64,6 +76,7 @@ function compactAssistantMessage(value) {
6476
if (!message || message.role !== 'assistant') return null
6577
return {
6678
role: 'assistant',
79+
content: compactAssistantContent(message.content),
6780
stopReason: asString(message.stopReason),
6881
errorMessage: asString(message.errorMessage),
6982
}

0 commit comments

Comments
 (0)