Skip to content

Commit d9a6769

Browse files
BillLeoutsakosvl346Bill Leoutsakos
andauthored
chore(pi): upgrade agent to 1.0.0 (#8585)
* chore(pi): upgrade agent to 1.0.0 * chore(pi): narrow upgrade regression coverage --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
1 parent 0c86cb6 commit d9a6769

16 files changed

Lines changed: 503 additions & 240 deletions
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { execFile } from 'node:child_process'
2+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
3+
import { tmpdir } from 'node:os'
4+
import { join } from 'node:path'
5+
import { promisify } from 'node:util'
6+
import { describe, expect, it } from 'vitest'
7+
import { PI_EVENT_FILTER_SOURCE } from '@/executor/handlers/pi/cloud/event-filter-source'
8+
import { buildPiScript } from '@/executor/handlers/pi/cloud/shared'
9+
import { PI_PACKAGE_VERSION } from '@/scripts/pi-sandbox-packages'
10+
11+
const exec = promisify(execFile)
12+
13+
async function invokeSandbox(version: string) {
14+
const root = await mkdtemp(join(tmpdir(), 'sim-pi-cli-'))
15+
const workspace = join(root, 'workspace')
16+
const repo = join(workspace, 'repo')
17+
const bin = join(root, 'bin')
18+
await mkdir(repo, { recursive: true })
19+
await mkdir(bin)
20+
await writeFile(join(workspace, 'pi-prompt.txt'), 'fixture prompt')
21+
await writeFile(join(workspace, 'sim-pi-event-filter.mjs'), PI_EVENT_FILTER_SOURCE)
22+
await writeFile(
23+
join(bin, 'pi'),
24+
`#!/bin/bash
25+
if [ "$1" = "--version" ]; then
26+
printf "%s\\n" "${version}"
27+
exit 0
28+
fi
29+
printf "%s" "$PI_CODING_AGENT_DIR" > "${root}/invoked"
30+
printf "%s\\n" '{"type":"message_update","assistantMessageEvent":{"type":"text_delta","delta":"ok"}}'
31+
exit 0
32+
`,
33+
{ mode: 0o700 }
34+
)
35+
const script = buildPiScript().replaceAll('/workspace', workspace)
36+
try {
37+
const result = await exec('/bin/bash', ['-c', script], {
38+
env: {
39+
...process.env,
40+
PATH: `${bin}:${process.env.PATH}`,
41+
PI_PROVIDER: 'fixture',
42+
PI_MODEL: 'fixture',
43+
PI_THINKING: 'off',
44+
},
45+
}).then(
46+
({ stdout, stderr }) => ({ code: 0, stdout, stderr }),
47+
(error: { code: number; stdout: string; stderr: string }) => error
48+
)
49+
const invoked = await readFile(join(root, 'invoked'), 'utf8').catch(() => null)
50+
const settings = invoked
51+
? await readFile(join(invoked, 'settings.json'), 'utf8').catch(() => null)
52+
: null
53+
const storedFiles = invoked ? await readdir(invoked).catch(() => []) : []
54+
return { ...result, invoked, settings, storedFiles, repo }
55+
} finally {
56+
await rm(root, { recursive: true, force: true })
57+
}
58+
}
59+
60+
describe('sandbox Pi runtime boundary', () => {
61+
it.each(['0.80.10', 'unexpected-version'])(
62+
'rejects %s before starting the agent',
63+
async (version) => {
64+
const result = await invokeSandbox(version)
65+
expect(result.code).not.toBe(0)
66+
expect(result.invoked).toBeNull()
67+
expect(result.stderr).toMatch(/rebuild|update/i)
68+
expect(result.stderr).toContain(PI_PACKAGE_VERSION)
69+
}
70+
)
71+
72+
it('uses private settings outside the repository with warming off and no saved credentials', async () => {
73+
const result = await invokeSandbox(PI_PACKAGE_VERSION)
74+
expect(result.code).toBe(0)
75+
expect(result.invoked).toBeTruthy()
76+
expect(result.invoked?.startsWith(result.repo)).toBe(false)
77+
expect(JSON.parse(result.settings ?? '{}')).toEqual({ cacheWarming: 'off' })
78+
expect(result.storedFiles).toEqual(['settings.json'])
79+
expect(JSON.parse(result.stdout.trim())).toEqual({
80+
type: 'message_update',
81+
assistantMessageEvent: { type: 'text_delta', delta: 'ok' },
82+
})
83+
})
84+
})

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
* The stdout filter the sandbox modes pipe the Pi CLI through, written at runtime like the search
33
* extension and the review tools script next to it.
44
*
5-
* Pi's `--mode json` writes `JSON.stringify(event)` for every session event with no filtering, and
6-
* `message_update` repeats the whole assistant message alongside each delta — so raw stdout grows
7-
* with the square of the response length, and `tool_execution_end`, `turn_end`, and `agent_end`
8-
* each add a full tool result, turn transcript, or run transcript on top of that.
5+
* Pi 1.0 emits delta-only `message_update` events, but `tool_execution_end`, `turn_end`, and
6+
* `agent_end` still include tool results and transcripts. The filter preserves Sim's event
7+
* contract without retaining those cumulative payloads.
98
*
109
* The reduction has to happen in the sandbox because by the time Sim could drop the bytes they are
1110
* already retained: E2B's SDK accumulates every callback-delivered chunk internally, so its adapter

‎apps/sim/executor/handlers/pi/cloud/review/backend.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const mockAgentSession = {
4545
const sealedResourceLoader = { kind: 'sealed' }
4646

4747
const mockSdk = {
48-
SettingsManager: { inMemory: vi.fn(() => ({})) },
48+
SettingsManager: { inMemory: () => ({}) },
4949
SessionManager: { inMemory: vi.fn(() => ({})) },
5050
createAgentSession: mockCreateAgentSession,
5151
defineTool: vi.fn((tool) => tool),

‎apps/sim/executor/handlers/pi/cloud/review/backend.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ export const runCloudReviewPi: PiBackendRun<PiCloudReviewRunParams> = async (par
300300
)
301301
}
302302

303-
const settingsManager = sdk.SettingsManager.inMemory()
303+
const settingsManager = sdk.SettingsManager.inMemory({ cacheWarming: 'off' })
304304
const resourceLoader = createSealedPiResourceLoader(
305305
sdk,
306306
buildReviewSystemPrompt(Boolean(searchTool))

‎apps/sim/executor/handlers/pi/cloud/shared.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
99
import { resolvePiSandboxLifetimeMs } from '@/lib/execution/remote-sandbox/pi-lifetime'
1010
import { PI_EVENT_FILTER_PATH } from '@/executor/handlers/pi/cloud/event-filter-source'
1111
import { scrubPiSecrets } from '@/executor/handlers/pi/core/redaction'
12+
import { PI_PACKAGE_VERSION } from '@/scripts/pi-sandbox-packages'
1213

1314
export const REPO_DIR = '/workspace/repo'
1415
export const PROMPT_PATH = '/workspace/pi-prompt.txt'
@@ -141,6 +142,10 @@ export const PUSH_SCRIPT = `cd ${REPO_DIR}
141142
* there first — skipping that write does not fall back to the raw stream, it fails the run on the
142143
* missing module.
143144
*
145+
* Rejects stale images before starting the agent. Each invocation resets Sim's private agent
146+
* directory outside the clone, so neither stored credentials nor cache-warming settings can leak
147+
* between rounds. Model credentials remain environment-only.
148+
*
144149
* Selects `/bin/bash` explicitly because `pipefail` is not portable to `/bin/sh`, and without it
145150
* the pipeline reports the filter's exit code rather than Pi's, so an upstream crash would read as
146151
* a clean run. Both dedicated Pi images are Debian-based and provide Bash, so provider
@@ -165,7 +170,17 @@ export function buildPiScript(
165170
? ' --no-extensions'
166171
: ''
167172
const extensionArgs = extensionPath ? ` -e ${extensionPath}` : ''
168-
return `/bin/bash -o pipefail -c 'cd ${REPO_DIR}
173+
return `/bin/bash -o pipefail -c 'set -e
174+
if ! PI_INSTALLED_VERSION="$(pi --version 2>/dev/null)" || [ "$PI_INSTALLED_VERSION" != "${PI_PACKAGE_VERSION}" ]; then
175+
printf "%s\\n" "Pi sandbox runtime must be ${PI_PACKAGE_VERSION}. Rebuild or update the configured Pi sandbox image before running this workflow." >&2
176+
exit 1
177+
fi
178+
export PI_CODING_AGENT_DIR=/workspace/sim-pi-agent
179+
(umask 077
180+
rm -rf "$PI_CODING_AGENT_DIR"
181+
mkdir -p "$PI_CODING_AGENT_DIR"
182+
printf "%s\\n" "{\\"cacheWarming\\":\\"off\\"}" > "$PI_CODING_AGENT_DIR/settings.json")
183+
cd ${REPO_DIR}
169184
pi -p --mode json --provider "$PI_PROVIDER" --model "$PI_MODEL" --thinking "$PI_THINKING"${repositoryArgs}${extensionArgs} < ${PROMPT_PATH} | node ${PI_EVENT_FILTER_PATH}'`
170185
}
171186

‎apps/sim/executor/handlers/pi/core/pi-sdk.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,9 @@ function isToolArguments(value: unknown): value is Record<string, unknown> {
2929
* credentials from thrown and reported tool errors. Successful tool output is ordinary model
3030
* content and stays verbatim; Sim-secret projection is owned by the tool adapter's provenance.
3131
*
32-
* A spec's `isError` is rethrown rather than reported in the result: Pi derives a call's error state
33-
* solely from whether `execute` threw, so a resolved failure would reach the model as a successful
34-
* tool call whose text happens to describe a failure. Throwing also matches Pi's own `bash`, which
35-
* throws on a non-zero exit with the output appended, so the failure text survives either way.
32+
* A spec's `isError` is rethrown with scrubbed text, preserving Sim's existing failure semantics.
33+
* Pi 1.0 also accepts an explicit error result, but throwing keeps the same diagnostic boundary
34+
* for reported failures and exceptions.
3635
*
3736
* Shared by both host-side backends: Local Dev converts its SSH, Sim, and search tools here, and
3837
* Review Code converts its search tool here alongside the review tools it builds directly.
@@ -90,7 +89,9 @@ export function createSealedPiResourceLoader(sdk: PiSdk, systemPrompt: string):
9089
getThemes: () => ({ themes: [], diagnostics: [] }),
9190
getAgentsFiles: () => ({ agentsFiles: [] }),
9291
getSystemPrompt: () => systemPrompt,
92+
getSystemPromptSource: () => undefined,
9393
getAppendSystemPrompt: () => [],
94+
getAppendSystemPromptSources: () => [],
9495
extendResources: () => {},
9596
reload: async () => {},
9697
}

‎apps/sim/executor/handlers/pi/local/backend.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const mockAgentSession = {
3939
}
4040
const mockSdk = {
4141
defineTool: vi.fn((tool) => tool),
42+
SettingsManager: { inMemory: () => ({}) },
4243
SessionManager: { inMemory: vi.fn(() => ({})) },
4344
createAgentSession: mockCreateAgentSession,
4445
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ async function runLocalAgent(
9393
noTools: 'builtin',
9494
customTools,
9595
modelRuntime,
96+
settingsManager: sdk.SettingsManager.inMemory({ cacheWarming: 'off' }),
9697
sessionManager: sdk.SessionManager.inMemory(isolatedDir),
9798
})
9899

‎apps/sim/executor/handlers/pi/pi-handler.test.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,13 +213,17 @@ describe('PiBlockHandler', () => {
213213
expect(mockRunLocal).not.toHaveBeenCalled()
214214
})
215215

216-
it('rejects an unavailable model before resolving credentials', async () => {
216+
it('rejects an unavailable model with replacement guidance before credentials or remote setup', async () => {
217217
mockResolvePiModelId.mockReturnValue(undefined)
218218

219219
await expect(handler.execute(ctx(), block, localInputs())).rejects.toThrow(
220-
/not available.*installed Pi catalog/
220+
/not available.*Choose a supported Pi model/
221221
)
222222
expect(mockResolveKey).not.toHaveBeenCalled()
223+
expect(mockRunLocal).not.toHaveBeenCalled()
224+
expect(mockRunCloud).not.toHaveBeenCalled()
225+
expect(mockRunCloudPlan).not.toHaveBeenCalled()
226+
expect(mockRunCloudReview).not.toHaveBeenCalled()
223227
})
224228

225229
it('adds successful Function tool cost once to a non-streaming Local Dev result', async () => {

‎apps/sim/executor/handlers/pi/pi-handler.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export class PiBlockHandler implements BlockHandler {
222222
const piModel = resolvePiModelId(providerId, model)
223223
if (!piModel) {
224224
throw new Error(
225-
`Pi model "${model}" is not available for provider "${providerId}" in the installed Pi catalog`
225+
`Pi model "${model}" is not available for provider "${providerId}" in the installed Pi catalog. Choose a supported Pi model in the block settings.`
226226
)
227227
}
228228

0 commit comments

Comments
 (0)