Skip to content

Commit a46df54

Browse files
committed
fix: stop V4 polling on cancellation
1 parent dd456be commit a46df54

3 files changed

Lines changed: 47 additions & 3 deletions

File tree

apps/sim/blocks/blocks/browser_use.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
1717
canvasPresentation: {
1818
defaultTitle: 'Browser Use',
1919
sentences: {
20+
default: [
21+
{ text: 'Run the browser task', field: 'task', core: true },
22+
{ text: ', starting at', field: 'startUrl' },
23+
],
2024
byOperation: {
2125
browser_use_run_task: [
2226
{ text: 'Run the legacy browser task', field: 'task', core: true },

apps/sim/tools/browser_use/run_v4.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,37 @@ describe('Browser Use V4 tool', () => {
114114
expect(fetchMock.mock.calls[2]?.[0]).toBe('https://api.browser-use.com/api/v4/runs/run-1')
115115
})
116116

117+
it('stops polling when the workflow is aborted', async () => {
118+
const controller = new AbortController()
119+
const fetchMock = vi
120+
.fn<typeof fetch>()
121+
.mockResolvedValueOnce(
122+
jsonResponse({
123+
id: 'run-1',
124+
status: 'queued',
125+
model: 'gpt-5.6-luna',
126+
sessionId: 'session-1',
127+
workspaceId: 'workspace-1',
128+
})
129+
)
130+
.mockImplementationOnce(async (_input, init) => {
131+
expect(init?.signal).toBe(controller.signal)
132+
controller.abort(new Error('Workflow cancelled'))
133+
return jsonResponse({ status: 'running' })
134+
})
135+
vi.stubGlobal('fetch', fetchMock)
136+
137+
const result = await runV4Tool.directExecution?.(
138+
{ task: 'Check the latest invoice', apiKey: 'test-key' },
139+
controller.signal
140+
)
141+
142+
expect(result?.success).toBe(false)
143+
expect(result?.error).toBe('Error running V4 agent: Workflow cancelled')
144+
expect(fetchMock).toHaveBeenCalledTimes(2)
145+
expect(fetchMock.mock.calls[0]?.[1]?.signal).toBe(controller.signal)
146+
})
147+
117148
it('returns the upstream create error without polling', async () => {
118149
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(
119150
new Response('Insufficient credits', {

apps/sim/tools/browser_use/run_v4.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,17 @@ async function readError(response: Response): Promise<string> {
8787

8888
async function pollForCompletion(
8989
runId: string,
90-
apiKey: string
90+
apiKey: string,
91+
signal?: AbortSignal
9192
): Promise<{ summary?: V4RunSummary; error?: string }> {
9293
const deadline = Date.now() + MAX_POLL_TIME_MS
9394

9495
for (;;) {
96+
signal?.throwIfAborted()
9597
const statusResponse = await fetch(`${API_BASE}/runs/${runId}/status`, {
9698
method: 'GET',
9799
headers: { 'X-Browser-Use-API-Key': apiKey },
100+
signal,
98101
})
99102
if (!statusResponse.ok) {
100103
return { error: `Failed to read run status: ${await readError(statusResponse)}` }
@@ -105,6 +108,7 @@ async function pollForCompletion(
105108
const runResponse = await fetch(`${API_BASE}/runs/${runId}`, {
106109
method: 'GET',
107110
headers: { 'X-Browser-Use-API-Key': apiKey },
111+
signal,
108112
})
109113
if (!runResponse.ok) {
110114
return { error: `Failed to read completed run: ${await readError(runResponse)}` }
@@ -115,6 +119,7 @@ async function pollForCompletion(
115119
if (Date.now() >= deadline) {
116120
return { error: `Run did not complete within ${MAX_POLL_TIME_MS / 1000}s` }
117121
}
122+
signal?.throwIfAborted()
118123
await sleep(POLL_INTERVAL_MS)
119124
}
120125
}
@@ -201,7 +206,10 @@ export const runV4Tool: ToolConfig<BrowserUseRunV4Params, BrowserUseRunV4Respons
201206
},
202207
},
203208

204-
directExecution: async (params: BrowserUseRunV4Params): Promise<ToolResponse> => {
209+
directExecution: async (
210+
params: BrowserUseRunV4Params,
211+
signal?: AbortSignal
212+
): Promise<ToolResponse> => {
205213
const body: Record<string, unknown> = { task: params.task }
206214
if (params.model) body.model = params.model
207215
if (params.sessionId) body.sessionId = params.sessionId
@@ -226,6 +234,7 @@ export const runV4Tool: ToolConfig<BrowserUseRunV4Params, BrowserUseRunV4Respons
226234
'X-Browser-Use-API-Key': params.apiKey,
227235
},
228236
body: JSON.stringify(body),
237+
signal,
229238
})
230239
if (!response.ok) {
231240
return {
@@ -237,7 +246,7 @@ export const runV4Tool: ToolConfig<BrowserUseRunV4Params, BrowserUseRunV4Respons
237246

238247
const created = (await response.json()) as V4RunCreateResponse
239248
logger.info(`Created Browser Use V4 run ${created.id}`)
240-
const completed = await pollForCompletion(created.id, params.apiKey)
249+
const completed = await pollForCompletion(created.id, params.apiKey, signal)
241250
if (!completed.summary) {
242251
return { success: false, output: emptyOutput(created), error: completed.error }
243252
}

0 commit comments

Comments
 (0)