executor.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
  2. import { AgentContext, type AgentOptions } from './types';
  3. import { NavigatorAgent, NavigatorActionRegistry } from './agents/navigator';
  4. import { PlannerAgent } from './agents/planner';
  5. import { ValidatorAgent } from './agents/validator';
  6. import { NavigatorPrompt } from './prompts/navigator';
  7. import { PlannerPrompt } from './prompts/planner';
  8. import { ValidatorPrompt } from './prompts/validator';
  9. import { createLogger } from '@src/background/log';
  10. import MessageManager from './messages/service';
  11. import type BrowserContext from '../browser/context';
  12. import { ActionBuilder } from './actions/builder';
  13. import { EventManager } from './event/manager';
  14. import { Actors, type EventCallback, EventType, ExecutionState } from './event/types';
  15. import { ChatModelAuthError } from './agents/errors';
  16. const logger = createLogger('Executor');
  17. export interface ExecutorExtraArgs {
  18. plannerLLM?: BaseChatModel;
  19. validatorLLM?: BaseChatModel;
  20. extractorLLM?: BaseChatModel;
  21. agentOptions?: Partial<AgentOptions>;
  22. }
  23. export class Executor {
  24. private readonly navigator: NavigatorAgent;
  25. private readonly planner: PlannerAgent;
  26. private readonly validator: ValidatorAgent;
  27. private readonly context: AgentContext;
  28. private readonly plannerPrompt: PlannerPrompt;
  29. private readonly navigatorPrompt: NavigatorPrompt;
  30. private readonly validatorPrompt: ValidatorPrompt;
  31. private tasks: string[] = [];
  32. constructor(
  33. task: string,
  34. taskId: string,
  35. browserContext: BrowserContext,
  36. navigatorLLM: BaseChatModel,
  37. extraArgs?: Partial<ExecutorExtraArgs>,
  38. ) {
  39. const messageManager = new MessageManager({});
  40. const plannerLLM = extraArgs?.plannerLLM ?? navigatorLLM;
  41. const validatorLLM = extraArgs?.validatorLLM ?? navigatorLLM;
  42. const extractorLLM = extraArgs?.extractorLLM ?? navigatorLLM;
  43. const eventManager = new EventManager();
  44. const context = new AgentContext(
  45. taskId,
  46. browserContext,
  47. messageManager,
  48. eventManager,
  49. extraArgs?.agentOptions ?? {},
  50. );
  51. this.tasks.push(task);
  52. this.navigatorPrompt = new NavigatorPrompt(context.options.maxActionsPerStep);
  53. this.plannerPrompt = new PlannerPrompt();
  54. this.validatorPrompt = new ValidatorPrompt(task);
  55. const actionBuilder = new ActionBuilder(context, extractorLLM);
  56. const navigatorActionRegistry = new NavigatorActionRegistry(actionBuilder.buildDefaultActions());
  57. // Initialize agents with their respective prompts
  58. this.navigator = new NavigatorAgent(navigatorActionRegistry, {
  59. chatLLM: navigatorLLM,
  60. context: context,
  61. prompt: this.navigatorPrompt,
  62. });
  63. this.planner = new PlannerAgent({
  64. chatLLM: plannerLLM,
  65. context: context,
  66. prompt: this.plannerPrompt,
  67. });
  68. this.validator = new ValidatorAgent({
  69. chatLLM: validatorLLM,
  70. context: context,
  71. prompt: this.validatorPrompt,
  72. });
  73. this.context = context;
  74. // Initialize message history
  75. this.context.messageManager.initTaskMessages(this.navigatorPrompt.getSystemMessage(), task);
  76. }
  77. subscribeExecutionEvents(callback: EventCallback): void {
  78. this.context.eventManager.subscribe(EventType.EXECUTION, callback);
  79. }
  80. clearExecutionEvents(): void {
  81. // Clear all execution event listeners
  82. this.context.eventManager.clearSubscribers(EventType.EXECUTION);
  83. }
  84. addFollowUpTask(task: string): void {
  85. this.tasks.push(task);
  86. this.context.messageManager.addNewTask(task);
  87. // update validator prompt
  88. this.validatorPrompt.addFollowUpTask(task);
  89. // need to reset previous action results that are not included in memory
  90. this.context.actionResults = this.context.actionResults.filter(result => result.includeInMemory);
  91. }
  92. /**
  93. * Execute the task
  94. *
  95. * @returns {Promise<void>}
  96. */
  97. async execute(): Promise<void> {
  98. logger.info(`🚀 Executing task: ${this.tasks[this.tasks.length - 1]}`);
  99. // reset the step counter
  100. const context = this.context;
  101. context.nSteps = 0;
  102. const allowedMaxSteps = this.context.options.maxSteps;
  103. try {
  104. this.context.emitEvent(Actors.SYSTEM, ExecutionState.TASK_START, this.context.taskId);
  105. let done = false;
  106. let step = 0;
  107. let validatorFailed = false;
  108. for (step = 0; step < allowedMaxSteps; step++) {
  109. context.stepInfo = {
  110. stepNumber: context.nSteps,
  111. maxSteps: context.options.maxSteps,
  112. };
  113. logger.info(`🔄 Step ${step + 1} / ${allowedMaxSteps}`);
  114. if (await this.shouldStop()) {
  115. break;
  116. }
  117. // Run planner if configured
  118. if (this.planner && (context.nSteps % context.options.planningInterval === 0 || validatorFailed)) {
  119. validatorFailed = false;
  120. // The first planning step is special, we don't want to add the browser state message to memory
  121. if (this.tasks.length > 1 || step > 0) {
  122. await this.navigator.addStateMessageToMemory();
  123. }
  124. const planOutput = await this.planner.execute();
  125. if (planOutput.result) {
  126. logger.info(`🔄 Planner output: ${JSON.stringify(planOutput.result, null, 2)}`);
  127. this.context.messageManager.addPlan(
  128. JSON.stringify(planOutput.result),
  129. this.context.messageManager.length() - 1,
  130. );
  131. if (planOutput.result.done) {
  132. // task is complete, skip navigation
  133. done = true;
  134. this.validator.setPlan(planOutput.result.next_steps);
  135. } else {
  136. // task is not complete, let's navigate
  137. this.validator.setPlan(null);
  138. done = false;
  139. }
  140. if (!planOutput.result.web_task && planOutput.result.done) {
  141. break;
  142. }
  143. }
  144. }
  145. // execute the navigation step
  146. if (!done) {
  147. done = await this.navigate();
  148. }
  149. // validate the output
  150. if (done && this.context.options.validateOutput && !this.context.stopped && !this.context.paused) {
  151. const validatorOutput = await this.validator.execute();
  152. if (validatorOutput.result?.is_valid) {
  153. logger.info('✅ Task completed successfully');
  154. break;
  155. }
  156. validatorFailed = true;
  157. }
  158. }
  159. if (done) {
  160. this.context.emitEvent(Actors.SYSTEM, ExecutionState.TASK_OK, this.context.taskId);
  161. } else if (step >= allowedMaxSteps) {
  162. logger.info('❌ Task failed: Max steps reached');
  163. this.context.emitEvent(Actors.SYSTEM, ExecutionState.TASK_FAIL, 'Task failed: Max steps reached');
  164. } else if (this.context.stopped) {
  165. this.context.emitEvent(Actors.SYSTEM, ExecutionState.TASK_CANCEL, 'Task cancelled');
  166. } else {
  167. this.context.emitEvent(Actors.SYSTEM, ExecutionState.TASK_PAUSE, 'Task paused');
  168. }
  169. } catch (error) {
  170. const errorMessage = error instanceof Error ? error.message : String(error);
  171. this.context.emitEvent(Actors.SYSTEM, ExecutionState.TASK_FAIL, `Task failed: ${errorMessage}`);
  172. }
  173. }
  174. private async navigate(): Promise<boolean> {
  175. const context = this.context;
  176. try {
  177. // Get and execute navigation action
  178. // check if the task is paused or stopped
  179. if (context.paused || context.stopped) {
  180. return false;
  181. }
  182. const navOutput = await this.navigator.execute();
  183. // check if the task is paused or stopped
  184. if (context.paused || context.stopped) {
  185. return false;
  186. }
  187. context.nSteps++;
  188. if (navOutput.error) {
  189. throw new Error(navOutput.error);
  190. }
  191. context.consecutiveFailures = 0;
  192. if (navOutput.result?.done) {
  193. return true;
  194. }
  195. } catch (error) {
  196. if (error instanceof ChatModelAuthError) {
  197. throw error;
  198. }
  199. context.consecutiveFailures++;
  200. logger.error(`Failed to execute step: ${error}`);
  201. if (context.consecutiveFailures >= context.options.maxFailures) {
  202. throw new Error('Max failures reached');
  203. }
  204. }
  205. return false;
  206. }
  207. private async shouldStop(): Promise<boolean> {
  208. if (this.context.stopped) {
  209. logger.info('Agent stopped');
  210. return true;
  211. }
  212. while (this.context.paused) {
  213. await new Promise(resolve => setTimeout(resolve, 200));
  214. if (this.context.stopped) {
  215. return true;
  216. }
  217. }
  218. if (this.context.consecutiveFailures >= this.context.options.maxFailures) {
  219. logger.error(`Stopping due to ${this.context.options.maxFailures} consecutive failures`);
  220. return true;
  221. }
  222. return false;
  223. }
  224. async cancel(): Promise<void> {
  225. this.context.stop();
  226. }
  227. async resume(): Promise<void> {
  228. this.context.resume();
  229. }
  230. async pause(): Promise<void> {
  231. this.context.pause();
  232. }
  233. async cleanup(): Promise<void> {
  234. try {
  235. await this.context.browserContext.cleanup();
  236. } catch (error) {
  237. logger.error(`Failed to cleanup browser context: ${error}`);
  238. }
  239. }
  240. async getCurrentTaskId(): Promise<string> {
  241. return this.context.taskId;
  242. }
  243. }