@@ -45,8 +45,19 @@ func NewWithConfig(config types.Config) *Executor {
4545 }
4646}
4747
48- // Execute runs a robot through all applicable phases with real Agent calls
48+ // Execute runs a robot through all applicable phases with real Agent calls (auto-generates ID)
4949func (e * Executor ) Execute (ctx * robottypes.Context , robot * robottypes.Robot , trigger robottypes.TriggerType , data interface {}) (* robottypes.Execution , error ) {
50+ return e .ExecuteWithControl (ctx , robot , trigger , data , "" , nil )
51+ }
52+
53+ // ExecuteWithID runs a robot through all applicable phases with a pre-generated execution ID (no control)
54+ func (e * Executor ) ExecuteWithID (ctx * robottypes.Context , robot * robottypes.Robot , trigger robottypes.TriggerType , data interface {}, execID string ) (* robottypes.Execution , error ) {
55+ return e .ExecuteWithControl (ctx , robot , trigger , data , execID , nil )
56+ }
57+
58+ // ExecuteWithControl runs a robot through all applicable phases with execution control
59+ // control: optional, allows pause/resume functionality during execution
60+ func (e * Executor ) ExecuteWithControl (ctx * robottypes.Context , robot * robottypes.Robot , trigger robottypes.TriggerType , data interface {}, execID string , control robottypes.ExecutionControl ) (* robottypes.Execution , error ) {
5061 if robot == nil {
5162 return nil , fmt .Errorf ("robot cannot be nil" )
5263 }
@@ -57,10 +68,15 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
5768 startPhaseIndex = 1 // Skip P0 (Inspiration)
5869 }
5970
71+ // Use provided execID or generate new one
72+ if execID == "" {
73+ execID = utils .NewID ()
74+ }
75+
6076 // Create execution (Job system removed, using ExecutionStore only)
6177 input := types .BuildTriggerInput (trigger , data )
6278 exec := & robottypes.Execution {
63- ID : utils . NewID () ,
79+ ID : execID ,
6480 MemberID : robot .MemberID ,
6581 TeamID : robot .TeamID ,
6682 TriggerType : trigger ,
@@ -174,7 +190,31 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
174190 // Execute phases
175191 phases := robottypes .AllPhases [startPhaseIndex :]
176192 for _ , phase := range phases {
177- if err := e .runPhase (ctx , exec , phase , data ); err != nil {
193+ if err := e .runPhase (ctx , exec , phase , data , control ); err != nil {
194+ // Check if execution was cancelled
195+ if err == robottypes .ErrExecutionCancelled {
196+ exec .Status = robottypes .ExecCancelled
197+ exec .Error = "execution cancelled by user"
198+ now := time .Now ()
199+ exec .EndTime = & now
200+
201+ // Update UI field for cancellation with i18n
202+ e .updateUIFields (ctx , exec , "" , getLocalizedMessage (locale , "cancelled" ))
203+
204+ log .With (log.F {
205+ "execution_id" : exec .ID ,
206+ "member_id" : exec .MemberID ,
207+ "phase" : string (phase ),
208+ }).Info ("Execution cancelled by user" )
209+
210+ // Persist cancelled status
211+ if ! e .config .SkipPersistence && e .store != nil {
212+ _ = e .store .UpdateStatus (ctx .Context , exec .ID , robottypes .ExecCancelled , "execution cancelled by user" )
213+ }
214+ return exec , nil
215+ }
216+
217+ // Normal failure case
178218 exec .Status = robottypes .ExecFailed
179219 exec .Error = err .Error ()
180220
@@ -228,7 +268,21 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
228268}
229269
230270// runPhase executes a single phase
231- func (e * Executor ) runPhase (ctx * robottypes.Context , exec * robottypes.Execution , phase robottypes.Phase , data interface {}) error {
271+ func (e * Executor ) runPhase (ctx * robottypes.Context , exec * robottypes.Execution , phase robottypes.Phase , data interface {}, control robottypes.ExecutionControl ) error {
272+ // Check if context is cancelled before starting this phase
273+ select {
274+ case <- ctx .Context .Done ():
275+ return robottypes .ErrExecutionCancelled
276+ default :
277+ }
278+
279+ // Wait if execution is paused (blocks until resumed or cancelled)
280+ if control != nil {
281+ if err := control .WaitIfPaused (); err != nil {
282+ return err // Returns ErrExecutionCancelled if cancelled while paused
283+ }
284+ }
285+
232286 exec .Phase = phase
233287
234288 log .With (log.F {
@@ -422,6 +476,7 @@ var uiMessages = map[string]map[string]string{
422476 "sending_delivery" : "Sending delivery..." ,
423477 "learning_from_exec" : "Learning from execution..." ,
424478 "completed" : "Completed" ,
479+ "cancelled" : "Cancelled" ,
425480 "failed_prefix" : "Failed at " ,
426481 "task_prefix" : "Task" ,
427482 // Phase names for failure messages
@@ -445,6 +500,7 @@ var uiMessages = map[string]map[string]string{
445500 "sending_delivery" : "正在发送..." ,
446501 "learning_from_exec" : "学习执行经验..." ,
447502 "completed" : "已完成" ,
503+ "cancelled" : "已取消" ,
448504 "failed_prefix" : "失败于" ,
449505 "task_prefix" : "任务" ,
450506 // Phase names for failure messages
0 commit comments