|
7 | 7 |
|
8 | 8 | import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' |
9 | 9 | import { resolvePiSandboxLifetimeMs } from '@/lib/execution/remote-sandbox/pi-lifetime' |
| 10 | +import { PI_EVENT_FILTER_PATH } from '@/executor/handlers/pi/cloud/event-filter-source' |
10 | 11 | import { scrubPiSecrets } from '@/executor/handlers/pi/core/redaction' |
11 | 12 |
|
12 | 13 | export const REPO_DIR = '/workspace/repo' |
13 | 14 | export const PROMPT_PATH = '/workspace/pi-prompt.txt' |
14 | 15 | export const DIFF_PATH = '/workspace/pi.diff' |
15 | 16 | export const COMMIT_MSG_PATH = '/workspace/pi-commit.txt' |
16 | 17 | export const PUSH_ERR_PATH = '/workspace/pi-push-err.txt' |
17 | | -export const PI_EVENT_FILTER_PATH = '/workspace/sim-pi-event-filter.mjs' |
18 | 18 | export const CLONE_TIMEOUT_MS = 10 * 60 * 1000 |
19 | 19 | export const FINALIZE_TIMEOUT_MS = 10 * 60 * 1000 |
20 | 20 | export const MAX_DIFF_BYTES = 200_000 |
@@ -129,132 +129,19 @@ export const PUSH_SCRIPT = `cd ${REPO_DIR} |
129 | 129 | /usr/bin/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" "HEAD:refs/heads/$BRANCH" >/dev/null 2>${PUSH_ERR_PATH} && echo "__PUSHED__=1"` |
130 | 130 |
|
131 | 131 | /** |
132 | | - * Reduces Pi's cumulative JSON event stream before either sandbox provider retains it. |
133 | | - * The emitted shapes contain only fields consumed by `normalizePiEvent`; in particular, |
134 | | - * every `message_update` drops the full message Pi repeats alongside its delta. |
135 | | - */ |
136 | | -export const PI_EVENT_FILTER_SOURCE = `function asRecord(value) { |
137 | | - return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : null |
138 | | -} |
139 | | -
|
140 | | -function asString(value) { |
141 | | - return typeof value === 'string' ? value : undefined |
142 | | -} |
143 | | -
|
144 | | -function compactUsage(value) { |
145 | | - const usage = asRecord(value) |
146 | | - if (!usage) return null |
147 | | - return { |
148 | | - input: usage.input, |
149 | | - output: usage.output, |
150 | | - inputTokens: usage.inputTokens, |
151 | | - outputTokens: usage.outputTokens, |
152 | | - prompt_tokens: usage.prompt_tokens, |
153 | | - completion_tokens: usage.completion_tokens, |
154 | | - } |
155 | | -} |
156 | | -
|
157 | | -function compactAssistantMessage(value) { |
158 | | - const message = asRecord(value) |
159 | | - if (!message || message.role !== 'assistant') return null |
160 | | - const content = Array.isArray(message.content) |
161 | | - ? message.content.flatMap((value) => { |
162 | | - const block = asRecord(value) |
163 | | - return block?.type === 'text' && typeof block.text === 'string' |
164 | | - ? [{ type: 'text', text: block.text }] |
165 | | - : [] |
166 | | - }) |
167 | | - : [] |
168 | | - return { |
169 | | - role: 'assistant', |
170 | | - content, |
171 | | - stopReason: asString(message.stopReason), |
172 | | - errorMessage: asString(message.errorMessage), |
173 | | - } |
174 | | -} |
175 | | -
|
176 | | -function compactEvent(value) { |
177 | | - const event = asRecord(value) |
178 | | - if (!event) return null |
179 | | -
|
180 | | - switch (event.type) { |
181 | | - case 'message_update': { |
182 | | - const update = asRecord(event.assistantMessageEvent) |
183 | | - if (update?.type !== 'text_delta' && update?.type !== 'thinking_delta') return null |
184 | | - return { |
185 | | - type: 'message_update', |
186 | | - assistantMessageEvent: { type: update.type, delta: asString(update.delta) }, |
187 | | - } |
188 | | - } |
189 | | - case 'tool_execution_start': |
190 | | - return { type: 'tool_execution_start', toolName: asString(event.toolName) } |
191 | | - case 'tool_execution_end': |
192 | | - return { |
193 | | - type: 'tool_execution_end', |
194 | | - toolName: asString(event.toolName), |
195 | | - isError: event.isError === true, |
196 | | - } |
197 | | - case 'turn_end': { |
198 | | - const message = asRecord(event.message) |
199 | | - const usage = compactUsage(event.usage) |
200 | | - const messageUsage = compactUsage(message?.usage) |
201 | | - if (!usage && !messageUsage) return null |
202 | | - return { |
203 | | - type: 'turn_end', |
204 | | - ...(usage ? { usage } : {}), |
205 | | - ...(messageUsage ? { message: { usage: messageUsage } } : {}), |
206 | | - } |
207 | | - } |
208 | | - case 'agent_end': { |
209 | | - if (event.willRetry === true) return { type: 'agent_end', willRetry: true } |
210 | | - const messages = Array.isArray(event.messages) ? event.messages : [] |
211 | | - let assistant = null |
212 | | - for (let index = messages.length - 1; index >= 0; index -= 1) { |
213 | | - assistant = compactAssistantMessage(messages[index]) |
214 | | - if (assistant) break |
215 | | - } |
216 | | - return { type: 'agent_end', messages: assistant ? [assistant] : [] } |
217 | | - } |
218 | | - case 'error': |
219 | | - return { |
220 | | - type: 'error', |
221 | | - error: asString(event.error), |
222 | | - message: asString(event.message), |
223 | | - } |
224 | | - default: |
225 | | - return null |
226 | | - } |
227 | | -} |
228 | | -
|
229 | | -function processLine(line) { |
230 | | - if (!line.trim()) return |
231 | | - try { |
232 | | - const event = compactEvent(JSON.parse(line)) |
233 | | - if (event) process.stdout.write(JSON.stringify(event) + '\\n') |
234 | | - } catch {} |
235 | | -} |
236 | | -
|
237 | | -process.stdin.setEncoding('utf8') |
238 | | -let buffer = '' |
239 | | -process.stdin.on('data', (chunk) => { |
240 | | - buffer += chunk |
241 | | - const lines = buffer.split('\\n') |
242 | | - buffer = lines.pop() ?? '' |
243 | | - for (const line of lines) processLine(line) |
244 | | -}) |
245 | | -process.stdin.on('end', () => processLine(buffer)) |
246 | | -` |
247 | | - |
248 | | -/** |
249 | | - * The Pi CLI invocation for the sandbox modes. Every caller receives the compact event stream; |
250 | | - * options only control which repository resources Pi may load. |
| 132 | + * The Pi CLI invocation for the sandbox modes, piped through the sandbox event filter. The command |
| 133 | + * names {@link PI_EVENT_FILTER_PATH}, so every caller must have written `PI_EVENT_FILTER_SOURCE` |
| 134 | + * there first — skipping that write does not fall back to the raw stream, it fails the run on the |
| 135 | + * missing module. |
251 | 136 | * |
252 | | - * Selects `/bin/bash` explicitly because `pipefail` is not portable to `/bin/sh`. Both dedicated |
253 | | - * Pi images are Debian-based and provide Bash, so provider default-shell behavior cannot change |
254 | | - * whether an upstream Pi failure reaches the caller. |
| 137 | + * Selects `/bin/bash` explicitly because `pipefail` is not portable to `/bin/sh`, and without it |
| 138 | + * the pipeline reports the filter's exit code rather than Pi's, so an upstream crash would read as |
| 139 | + * a clean run. Both dedicated Pi images are Debian-based and provide Bash, so provider |
| 140 | + * default-shell behavior cannot change whether an upstream Pi failure reaches the caller. |
255 | 141 | * |
256 | | - * With one, `--no-extensions` drops any extension the cloned repository ships while leaving the |
257 | | - * explicit `-e` path loaded, so the loaded set is exactly Sim's own extension. That is deliberate — |
| 142 | + * With no options the repository resources Pi loads are exactly what Create PR always had. With an |
| 143 | + * `extensionPath`, `--no-extensions` drops any extension the cloned repository ships while leaving |
| 144 | + * the explicit `-e` path loaded, so the loaded set is exactly Sim's own extension. That is deliberate — |
258 | 145 | * a repository must not be able to register tools into a run holding the workspace's keys — but it |
259 | 146 | * does mean enabling search also stops loading a repository's own Pi extensions, which is why the |
260 | 147 | * flag is not passed on Create PR's no-search path. Babysit supplies |
|
0 commit comments