Skip to content

Commit a1bb35d

Browse files
Samiya CaurDevtools-frontend LUCI CQ
authored andcommitted
[AI Assistance] Cleanly handle abort in case of paused side effect
Bug: 380394373 Change-Id: I122f644d31746bf4a2f4c7951d39d8075b761be6 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6099097 Auto-Submit: Samiya Caur <[email protected]> Commit-Queue: Alex Rudenko <[email protected]> Reviewed-by: Alex Rudenko <[email protected]>
1 parent e9b3f50 commit a1bb35d

File tree

3 files changed

+62
-9
lines changed

3 files changed

+62
-9
lines changed

front_end/panels/ai_assistance/agents/AiAgent.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,8 @@ export abstract class AiAgent<T> {
339339
return request;
340340
}
341341

342-
handleAction(action: string): AsyncGenerator<SideEffectResponse, ActionResponse, void>;
342+
handleAction(action: string, options?: {signal?: AbortSignal}):
343+
AsyncGenerator<SideEffectResponse, ActionResponse, void>;
343344
handleAction(): never {
344345
throw new Error('Unexpected action found');
345346
}
@@ -510,10 +511,7 @@ STOP`;
510511
debugLog('Error calling the AIDA API', err);
511512

512513
if (err instanceof Host.AidaClient.AidaAbortError) {
513-
const response = {
514-
type: ResponseType.ERROR,
515-
error: ErrorType.ABORT,
516-
} as const;
514+
const response = this.#createAbortResponse();
517515
this.#addHistory(response);
518516
yield response;
519517
break;
@@ -597,7 +595,13 @@ STOP`;
597595
}
598596

599597
if (action) {
600-
const result = yield* this.handleAction(action);
598+
const result = yield* this.handleAction(action, {signal: options.signal});
599+
if (options?.signal?.aborted) {
600+
const response = this.#createAbortResponse();
601+
this.#addHistory(response);
602+
yield response;
603+
break;
604+
}
601605
this.#addHistory(result);
602606
query = `OBSERVATION: ${result.output}`;
603607
// Capture history state for the next iteration query.
@@ -631,6 +635,13 @@ STOP`;
631635
yield entry;
632636
}
633637
}
638+
639+
#createAbortResponse(): ResponseData {
640+
return {
641+
type: ResponseType.ERROR,
642+
error: ErrorType.ABORT,
643+
};
644+
}
634645
}
635646

636647
export function isDebugMode(): boolean {

front_end/panels/ai_assistance/agents/StylingAgent.test.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '../../../testing/EnvironmentHelpers.js';
1212
import * as AiAssistance from '../ai_assistance.js';
1313

14-
const {StylingAgent} = AiAssistance;
14+
const {StylingAgent, ErrorType} = AiAssistance;
1515

1616
describeWithEnvironment('StylingAgent', () => {
1717
function mockHostConfig(
@@ -693,7 +693,7 @@ c`;
693693

694694
count++;
695695
}
696-
const execJs = sinon.mock().twice();
696+
const execJs = sinon.mock().once();
697697
execJs.onCall(0).throws(new AiAssistance.SideEffectError('EvalError: Possible side-effect in debug-evaluate'));
698698
const agent = new StylingAgent({
699699
aidaClient: mockAidaClient(generateActionAndAnswer),
@@ -710,6 +710,42 @@ c`;
710710
assert.strictEqual(actionStep.output, 'Error: User denied code execution with side effects.');
711711
assert.strictEqual(execJs.getCalls().length, 1);
712712
});
713+
714+
it('returns error when side effect is aborted', async () => {
715+
const promise = Promise.withResolvers();
716+
const stub = sinon.stub().returns(promise);
717+
async function* generateAction() {
718+
yield {
719+
explanation: `ACTION
720+
$0.style.backgroundColor = 'red'
721+
STOP`,
722+
metadata: {},
723+
completed: true,
724+
};
725+
}
726+
const execJs = sinon.mock().once().throws(
727+
new AiAssistance.SideEffectError('EvalError: Possible side-effect in debug-evaluate'));
728+
const agent = new StylingAgent({
729+
aidaClient: mockAidaClient(generateAction),
730+
createExtensionScope,
731+
confirmSideEffectForTest: stub,
732+
execJs,
733+
});
734+
const controller = new AbortController();
735+
736+
const agentPromise = Array.fromAsync(
737+
agent.run('test', {selected: new AiAssistance.NodeContext(element), signal: controller.signal}));
738+
await stub.calledOnce;
739+
const awaitTimeout = (delay: number) => new Promise(resolve => setTimeout(resolve, delay));
740+
await awaitTimeout(10).then(() => controller.abort());
741+
const responses = await agentPromise;
742+
743+
const errorStep = responses.at(-1) as AiAssistance.ErrorResponse;
744+
assert.strictEqual(execJs.getCalls().length, 1);
745+
assert.exists(errorStep);
746+
assert.strictEqual(errorStep.error, ErrorType.ABORT);
747+
await promise.promise.then(value => assert.strictEqual(value, false));
748+
});
713749
});
714750

715751
describe('long `Observation` text handling', () => {

front_end/panels/ai_assistance/agents/StylingAgent.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,9 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
582582
return output.trim();
583583
}
584584

585-
override async * handleAction(action: string): AsyncGenerator<SideEffectResponse, ActionResponse, void> {
585+
override async *
586+
handleAction(action: string, options?: {signal?: AbortSignal}):
587+
AsyncGenerator<SideEffectResponse, ActionResponse, void> {
586588
debugLog(`Action to execute: ${action}`);
587589
if (this.executionMode === Root.Runtime.HostConfigFreestylerExecutionMode.NO_SCRIPTS) {
588590
return {
@@ -621,6 +623,10 @@ export class StylingAgent extends AiAgent<SDK.DOMModel.DOMNode> {
621623

622624
const sideEffectConfirmationPromiseWithResolvers = this.#confirmSideEffect<boolean>();
623625

626+
options?.signal?.addEventListener('abort', () => {
627+
sideEffectConfirmationPromiseWithResolvers.resolve(false);
628+
}, {once: true});
629+
624630
yield {
625631
type: ResponseType.SIDE_EFFECT,
626632
code: action,

0 commit comments

Comments
 (0)