Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/askui-nodejs/src/execution/dsl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('DSL', () => {
const testCommandSpy = jest.spyOn(underTest, 'fluentCommandExecutor');

await underTest.click().button().exec();
expect(testCommandSpy).toHaveBeenCalledWith('Click on button', [], { aiElementNames: [], customElementsJson: [] }, false);
expect(testCommandSpy).toHaveBeenCalledWith('Click on button', [], { aiElementNames: [], customElementsJson: [] }, false, undefined);
});

test('should call exec function with one custom element', async () => {
Expand Down Expand Up @@ -52,6 +52,7 @@ describe('DSL', () => {
],
},
false,
undefined,
);
});

Expand Down Expand Up @@ -96,6 +97,7 @@ describe('DSL', () => {
],
},
false,
undefined,
);
});

Expand Down Expand Up @@ -129,6 +131,7 @@ describe('DSL', () => {
],
},
false,
undefined,
);
});

Expand All @@ -152,6 +155,7 @@ describe('DSL', () => {
customElementsJson: [],
},
false,
undefined,
);
});
});
Expand Down
25 changes: 13 additions & 12 deletions packages/askui-nodejs/src/execution/dsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { CustomElementJson } from '../core/model/custom-element-json';
import { DetectedElement } from '../core/model/annotation-result/detected-element';
import { ModelCompositionBranch } from './model-composition-branch';
import { RetryStrategy } from './retry-strategies/retry-strategy';

function isStackTraceCodeline(line: string): boolean {
return /[ \t]+at .+/.test(line);
Expand All @@ -20,23 +21,18 @@ function splitStackTrace(stacktrace: string): { head: string[], codelines: strin
return { head: errorStacktraceHead, codelines: errorStacktraceCodeLines };
}
function rewriteStackTraceForError(error: Error, newStackTrace: string) {
const errorCopy = new Error(error.message);

if (!error.stack) {
errorCopy.stack = newStackTrace;
return errorCopy;
}
const errorStacktraceSplit = splitStackTrace(error.stack);
const errorStacktraceSplit = splitStackTrace(error.stack ?? '');
const newStacktraceSplit = splitStackTrace(newStackTrace);

errorCopy.stack = [
// eslint-disable-next-line no-param-reassign
error.stack = [
...errorStacktraceSplit.head,
...newStacktraceSplit.codelines,
' ',
...errorStacktraceSplit.head,
...errorStacktraceSplit.codelines,
].join('\n');
return errorCopy;
return error;
}

export enum Separators {
Expand All @@ -62,6 +58,7 @@ export interface CommandExecutorContext {
export interface ExecOptions {
modelComposition?: ModelCompositionBranch[];
skipCache?: boolean;
retryStrategy?: RetryStrategy;
}

abstract class FluentBase {
Expand All @@ -87,6 +84,7 @@ abstract class FluentBase {
protected fluentCommandStringBuilder(
modelComposition: ModelCompositionBranch[] = [],
skipCache = false,
retryStrategy?: RetryStrategy,
currentInstruction = '',
paramsList: Map<string, unknown[]> = new Map<string, unknown[]>(),
): Promise<void> {
Expand All @@ -104,6 +102,7 @@ abstract class FluentBase {
aiElementNames,
},
skipCache,
retryStrategy,
);
}
if (!this.prev) {
Expand All @@ -112,6 +111,7 @@ abstract class FluentBase {
return this.prev.fluentCommandStringBuilder(
modelComposition,
skipCache,
retryStrategy,
newCurrentInstruction,
newParamsList,
);
Expand Down Expand Up @@ -150,14 +150,14 @@ abstract class FluentBase {
}

export interface Executable {
exec(): Promise<void>
exec(execOptions?: ExecOptions): Promise<void>
}

export class Exec extends FluentBase implements Executable {
exec(execOptions?: ExecOptions): Promise<void> {
const originStacktrace = { stack: '' };
Error.captureStackTrace(originStacktrace, this.exec);
return this.fluentCommandStringBuilder(execOptions?.modelComposition, execOptions?.skipCache).catch((err: Error) => Promise.reject(rewriteStackTraceForError(err, originStacktrace.stack)));
return this.fluentCommandStringBuilder(execOptions?.modelComposition, execOptions?.skipCache, execOptions?.retryStrategy).catch((err: Error) => Promise.reject(rewriteStackTraceForError(err, originStacktrace.stack)));
}
}

Expand Down Expand Up @@ -1242,7 +1242,7 @@ export class FluentFiltersOrRelations extends FluentFilters {
exec(execOptions?: ExecOptions): Promise<void> {
const originStacktrace = { stack: '' };
Error.captureStackTrace(originStacktrace, this.exec);
return this.fluentCommandStringBuilder(execOptions?.modelComposition, execOptions?.skipCache).catch((err: Error) => Promise.reject(rewriteStackTraceForError(err, originStacktrace.stack)));
return this.fluentCommandStringBuilder(execOptions?.modelComposition, execOptions?.skipCache, execOptions?.retryStrategy).catch((err: Error) => Promise.reject(rewriteStackTraceForError(err, originStacktrace.stack)));
}
}

Expand Down Expand Up @@ -3147,6 +3147,7 @@ export abstract class FluentCommand extends FluentBase {
modelComposition: ModelCompositionBranch[],
context: CommandExecutorContext,
skipCache : boolean,
retryStrategy?: RetryStrategy,
): Promise<void>;
}

Expand Down
10 changes: 7 additions & 3 deletions packages/askui-nodejs/src/execution/execution-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ export class ExecutionRuntime {
instruction: Instruction,
modelComposition: ModelCompositionBranch[],
skipCache = false,
retryStrategy?: RetryStrategy,
): Promise<void> {
const controlCommand = await this.predictCommandWithRetry(
instruction,
modelComposition,
skipCache,
retryStrategy,
);
if (controlCommand.code === ControlCommandCode.OK) {
return this.requestControl(controlCommand);
Expand Down Expand Up @@ -114,15 +116,17 @@ export class ExecutionRuntime {
instruction: Instruction,
modelComposition: ModelCompositionBranch[],
skipCache = false,
retryStrategy?: RetryStrategy,
): Promise<ControlCommand> {
const strategy = retryStrategy ?? this.retryStrategy;
let command = await this.predictCommand(instruction, modelComposition, skipCache);
/* eslint-disable no-await-in-loop */
for (let k = 0; k < this.retryStrategy.retryCount; k += 1) {
for (let k = 0; k < strategy.retryCount; k += 1) {
if (command.code === ControlCommandCode.OK) {
return command;
}
const msUntilRetry = this.retryStrategy.getDelay(k + 1);
logger.debug(`Wait ${msUntilRetry} and retry predicting command...`);
const msUntilRetry = strategy.getDelay(k + 1);
logger.debug(`Wait ${msUntilRetry} and retry ${k + 1}/${strategy.retryCount} predicting command...`);
await delay(msUntilRetry);
command = await this.predictCommand(instruction, modelComposition, skipCache, new ControlCommandError(command.actions?.[0]?.text ?? ''));
}
Expand Down
45 changes: 35 additions & 10 deletions packages/askui-nodejs/src/execution/ui-control-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import { Instruction, StepReporter } from '../core/reporting';
import { AIElementCollection } from '../core/ai-element/ai-element-collection';
import { ModelCompositionBranch } from './model-composition-branch';
import { AIElementArgs } from '../core/ai-element/ai-elements-args';
import { NoRetryStrategy } from './retry-strategies';
import { NoRetryStrategy, RetryStrategy } from './retry-strategies';
import { ControlCommandError } from './control-command-error';
import { AskUIAgent, AgentHistory, ActOptions } from '../core/models/anthropic';
import { AskUIGetAskUIElementTool, AskUIListAIElementTool } from '../core/models/anthropic/tools/askui-api-tools';

Expand Down Expand Up @@ -215,6 +216,7 @@ export class UiControlClient extends ApiCommands {
modelComposition: ModelCompositionBranch[],
context: CommandExecutorContext = { customElementsJson: [], aiElementNames: [] },
skipCache = false,
retryStrategy?: RetryStrategy,
): Promise<void> {
const aiElements = await this.getAIElementsByNames(context.aiElementNames);
const instruction = await this.buildInstruction(
Expand All @@ -227,7 +229,12 @@ export class UiControlClient extends ApiCommands {
logger.debug(instruction);
try {
this.stepReporter.resetStep(instruction);
await this.executionRuntime.executeInstruction(instruction, modelComposition, skipCache);
await this.executionRuntime.executeInstruction(
instruction,
modelComposition,
skipCache,
retryStrategy,
);
await this.afterCommandExecution(instruction);
return await Promise.resolve();
} catch (error) {
Expand Down Expand Up @@ -523,17 +530,35 @@ export class UiControlClient extends ApiCommands {
* @param {number} waitTime - Time in milliseconds
*/
async waitUntil(AskUICommand: Executable, maxTry = 5, waitTime = 2000) {
logger.debug(`waitUntil: Starting with maxTry=${maxTry}, waitTime=${waitTime}ms, retryStrategy=${this.executionRuntime.retryStrategy.constructor.name}`);

const userDefinedStrategy = this.executionRuntime.retryStrategy;
try {
this.executionRuntime.retryStrategy = new NoRetryStrategy();
await AskUICommand.exec();
this.executionRuntime.retryStrategy = userDefinedStrategy;
} catch (error) {
if (maxTry === 0) {
this.executionRuntime.retryStrategy = new NoRetryStrategy();

const attempt = async (retriesLeft: number): Promise<void> => {
const attemptNumber = maxTry - retriesLeft;
logger.debug(`waitUntil: Attempt ${attemptNumber}/${maxTry} (${retriesLeft} retries remaining)`);
try {
await AskUICommand.exec();
logger.debug(`waitUntil: Command succeeded on attempt ${attemptNumber}/${maxTry}`);
return;
} catch (error: unknown) {
if (error instanceof ControlCommandError && retriesLeft > 0) {
logger.debug(`waitUntil: ControlCommandError on attempt ${attemptNumber}/${maxTry}, waiting ${waitTime}ms before retry.`, error);
await this.waitFor(waitTime).exec();
await attempt(retriesLeft - 1);
return;
}
const errorName = error instanceof Error ? error.name : 'Error';
logger.debug(`waitUntil: ${errorName} on attempt ${attemptNumber}/${maxTry}, no retries remaining.`, error);
throw error;
}
await this.waitFor(waitTime).exec();
await this.waitUntil(AskUICommand, maxTry - 1, waitTime);
};

try {
await attempt(maxTry - 1);
} finally {
this.executionRuntime.retryStrategy = userDefinedStrategy;
}
}

Expand Down