@@ -201,6 +201,83 @@ func (c *executionController) handleSync(ctx *gin.Context) {
201201 }
202202
203203 resultBody , elapsed , asyncAccepted , callErr := c .callAgent (reqCtx , plan )
204+
205+ // If agent returned HTTP 202 (async acknowledgment), wait for callback completion
206+ if callErr == nil && asyncAccepted {
207+ logger .Logger .Info ().
208+ Str ("execution_id" , plan .exec .ExecutionID ).
209+ Str ("agent" , plan .target .NodeID ).
210+ Str ("reasoner" , plan .target .TargetName ).
211+ Msg ("agent returned async acknowledgment, waiting for completion" )
212+
213+ // Wait for agent to call back and complete the execution
214+ // Use 90 second timeout to match the HTTP client timeout
215+ exec , waitErr := c .waitForExecutionCompletion (reqCtx , plan .exec .ExecutionID , 90 * time .Second )
216+ if waitErr != nil {
217+ logger .Logger .Error ().
218+ Err (waitErr ).
219+ Str ("execution_id" , plan .exec .ExecutionID ).
220+ Msg ("failed to wait for async execution completion" )
221+ writeExecutionError (ctx , waitErr )
222+ return
223+ }
224+
225+ // Build response from completed execution
226+ var result interface {}
227+ if exec .ResultPayload != nil {
228+ result = decodeJSON (exec .ResultPayload )
229+ }
230+
231+ var durationMS int64
232+ if exec .DurationMS != nil {
233+ durationMS = * exec .DurationMS
234+ }
235+
236+ var finishedAt string
237+ if exec .CompletedAt != nil {
238+ finishedAt = exec .CompletedAt .UTC ().Format (time .RFC3339 )
239+ } else {
240+ finishedAt = time .Now ().UTC ().Format (time .RFC3339 )
241+ }
242+
243+ // Check if execution failed
244+ if exec .Status == types .ExecutionStatusFailed {
245+ errMsg := "execution failed"
246+ if exec .ErrorMessage != nil {
247+ errMsg = * exec .ErrorMessage
248+ }
249+ response := ExecuteResponse {
250+ ExecutionID : exec .ExecutionID ,
251+ RunID : exec .RunID ,
252+ Status : string (exec .Status ),
253+ ErrorMessage : & errMsg ,
254+ DurationMS : durationMS ,
255+ FinishedAt : finishedAt ,
256+ WebhookRegistered : exec .WebhookRegistered ,
257+ }
258+ ctx .Header ("X-Execution-ID" , exec .ExecutionID )
259+ ctx .Header ("X-Run-ID" , exec .RunID )
260+ ctx .JSON (http .StatusOK , response )
261+ return
262+ }
263+
264+ // Return successful execution result
265+ response := ExecuteResponse {
266+ ExecutionID : exec .ExecutionID ,
267+ RunID : exec .RunID ,
268+ Status : string (exec .Status ),
269+ Result : result ,
270+ DurationMS : durationMS ,
271+ FinishedAt : finishedAt ,
272+ WebhookRegistered : exec .WebhookRegistered ,
273+ }
274+ ctx .Header ("X-Execution-ID" , exec .ExecutionID )
275+ ctx .Header ("X-Run-ID" , exec .RunID )
276+ ctx .JSON (http .StatusOK , response )
277+ return
278+ }
279+
280+ // Agent returned HTTP 200 (synchronous result), process completion normally
204281 job := completionJob {
205282 controller : c ,
206283 plan : plan ,
@@ -209,26 +286,6 @@ func (c *executionController) handleSync(ctx *gin.Context) {
209286 callErr : callErr ,
210287 done : make (chan error , 1 ),
211288 }
212- if callErr == nil && asyncAccepted {
213- response := AsyncExecuteResponse {
214- ExecutionID : plan .exec .ExecutionID ,
215- RunID : plan .exec .RunID ,
216- WorkflowID : plan .exec .RunID ,
217- Status : string (types .ExecutionStatusRunning ),
218- Target : fmt .Sprintf ("%s.%s" , plan .target .NodeID , plan .target .TargetName ),
219- Type : plan .targetType ,
220- CreatedAt : plan .exec .CreatedAt .UTC ().Format (time .RFC3339 ),
221- EnqueuedAt : plan .exec .CreatedAt .UTC ().Format (time .RFC3339 ),
222- WebhookRegistered : plan .webhookRegistered ,
223- }
224- if plan .webhookError != nil {
225- response .WebhookError = plan .webhookError
226- }
227- ctx .Header ("X-Execution-ID" , plan .exec .ExecutionID )
228- ctx .Header ("X-Run-ID" , plan .exec .RunID )
229- ctx .JSON (http .StatusAccepted , response )
230- return
231- }
232289 if err := enqueueCompletion (job ); err != nil {
233290 logger .Logger .Error ().Err (err ).Str ("execution_id" , plan .exec .ExecutionID ).Msg ("failed to enqueue completion job" )
234291 writeExecutionError (ctx , err )
@@ -505,6 +562,72 @@ func (c *executionController) publishExecutionEvent(exec *types.Execution, statu
505562 c .eventBus .Publish (event )
506563}
507564
565+ // waitForExecutionCompletion waits for an execution to complete by subscribing to the event bus.
566+ // It returns the completed execution record or an error if the execution fails or times out.
567+ // This is used when agents return HTTP 202 (async acknowledgment) but the sync endpoint needs to wait for completion.
568+ func (c * executionController ) waitForExecutionCompletion (ctx context.Context , executionID string , timeout time.Duration ) (* types.Execution , error ) {
569+ if c .eventBus == nil {
570+ return nil , fmt .Errorf ("event bus not available" )
571+ }
572+
573+ // Create unique subscriber ID for this wait operation
574+ subscriberID := fmt .Sprintf ("sync-wait-%s" , executionID )
575+
576+ // Subscribe to events
577+ eventChan := c .eventBus .Subscribe (subscriberID )
578+ defer c .eventBus .Unsubscribe (subscriberID )
579+
580+ // Create timeout timer
581+ timer := time .NewTimer (timeout )
582+ defer timer .Stop ()
583+
584+ logger .Logger .Debug ().
585+ Str ("execution_id" , executionID ).
586+ Dur ("timeout" , timeout ).
587+ Msg ("waiting for execution completion via event bus" )
588+
589+ for {
590+ select {
591+ case <- ctx .Done ():
592+ return nil , ctx .Err ()
593+
594+ case <- timer .C :
595+ logger .Logger .Warn ().
596+ Str ("execution_id" , executionID ).
597+ Dur ("timeout" , timeout ).
598+ Msg ("execution completion timeout" )
599+ return nil , fmt .Errorf ("execution timeout after %v" , timeout )
600+
601+ case event := <- eventChan :
602+ // Only process events for this specific execution
603+ if event .ExecutionID != executionID {
604+ continue
605+ }
606+
607+ // Check if this is a terminal event
608+ if event .Type == events .ExecutionCompleted || event .Type == events .ExecutionFailed {
609+ logger .Logger .Debug ().
610+ Str ("execution_id" , executionID ).
611+ Str ("event_type" , string (event .Type )).
612+ Msg ("received terminal execution event" )
613+
614+ // Fetch the updated execution record
615+ exec , err := c .store .GetExecutionRecord (ctx , executionID )
616+ if err != nil {
617+ return nil , fmt .Errorf ("failed to fetch execution after completion: %w" , err )
618+ }
619+ if exec == nil {
620+ return nil , fmt .Errorf ("execution %s not found after completion event" , executionID )
621+ }
622+
623+ return exec , nil
624+ }
625+
626+ // Continue waiting for other event types (ExecutionUpdated, etc.)
627+ }
628+ }
629+ }
630+
508631type preparedExecution struct {
509632 exec * types.Execution
510633 requestBody []byte
0 commit comments