@@ -10,6 +10,7 @@ import { AgentLoggerService } from '../../agents/monitoring/index.js';
1010import type { InputContext } from '../input/index.js' ;
1111import { getUniqueAgentId } from '../context/index.js' ;
1212import { runStepResume } from '../step/run.js' ;
13+ import { resolveInteractiveBehavior } from '../step/interactive.js' ;
1314import type { RunnerContext } from './types.js' ;
1415
1516export interface WaitCallbacks {
@@ -18,48 +19,76 @@ export interface WaitCallbacks {
1819
1920/**
2021 * Handle waiting state - get input from provider
22+ *
23+ * Uses resolveInteractiveBehavior() for all 8 scenarios:
24+ * - Scenarios 1-4: Wait for controller/user input (shouldWait=true)
25+ * - Scenario 5: Run autonomous prompt loop (runAutonomousLoop=true)
26+ * - Scenario 6: Auto-advance to next step (queue exhausted after autonomous loop)
27+ * - Scenarios 7-8: Invalid cases forced to interactive:true (shouldWait=true)
2128 */
2229export async function handleWaiting ( ctx : RunnerContext , callbacks : WaitCallbacks ) : Promise < void > {
2330 const machineCtx = ctx . machine . context ;
2431
2532 debug ( '[Runner] Handling waiting state, autoMode=%s, paused=%s, promptQueue=%d items, queueIndex=%d' ,
2633 ctx . mode . autoMode , ctx . mode . paused , ctx . indexManager . promptQueue . length , ctx . indexManager . promptQueueIndex ) ;
2734
28- // Get provider from WorkflowMode (single source of truth)
29- // WorkflowMode.getActiveProvider() automatically handles paused and autoMode state
30- const provider = ctx . mode . getActiveProvider ( ) ;
31- if ( ctx . mode . paused ) {
32- debug ( '[Runner] Workflow is paused, using user input provider' ) ;
33- } else if ( ! ctx . mode . autoMode ) {
34- debug ( '[Runner] Manual mode, using user input provider' ) ;
35- }
36-
3735 // Get queue state from session (uses indexManager as single source of truth)
3836 const session = ctx . getCurrentSession ( ) ;
3937 const hasChainedPrompts = session
4038 ? ! session . isQueueExhausted
4139 : ! ctx . indexManager . isQueueExhausted ( ) ;
4240
43- if ( ! ctx . mode . paused && ! hasChainedPrompts && ctx . mode . autoMode ) {
44- // Check if we're resuming a step (sessionId exists but no completedAt)
45- const stepData = await ctx . indexManager . getStepData ( machineCtx . currentStepIndex ) ;
46- const isResumingStep = stepData ?. sessionId && ! stepData . completedAt ;
41+ // Get current step and resolve interactive behavior
42+ const step = ctx . moduleSteps [ machineCtx . currentStepIndex ] ;
43+ const stepUniqueAgentId = getUniqueAgentId ( step , machineCtx . currentStepIndex ) ;
4744
48- if ( isResumingStep ) {
49- // Resuming incomplete step - let controller decide
50- debug ( '[Runner] Resuming step in auto mode, letting controller handle it' ) ;
51- } else {
52- // No chained prompts, not paused, and in auto mode - auto-advance to next step
53- debug ( '[Runner] No chained prompts (auto mode), auto-advancing to next step' ) ;
54- await ctx . indexManager . stepCompleted ( machineCtx . currentStepIndex ) ;
55- ctx . machine . send ( { type : 'INPUT_RECEIVED' , input : '' } ) ;
56- return ;
45+ // Resolve interactive behavior using single source of truth
46+ const behavior = resolveInteractiveBehavior ( {
47+ step,
48+ autoMode : ctx . mode . autoMode ,
49+ hasChainedPrompts,
50+ stepIndex : machineCtx . currentStepIndex ,
51+ } ) ;
52+
53+ debug ( '[Runner] Scenario=%d, shouldWait=%s, runAutonomousLoop=%s, wasForced=%s' ,
54+ behavior . scenario , behavior . shouldWait , behavior . runAutonomousLoop , behavior . wasForced ) ;
55+
56+ // Handle Scenarios 7-8: interactive:false in manual mode
57+ // Behave like normal manual mode: ensure agent is awaiting and show prompt box
58+ if ( behavior . wasForced ) {
59+ ctx . emitter . logMessage ( stepUniqueAgentId , 'Manual mode active. Waiting for your input to continue. Use auto mode for fully autonomous execution.' ) ;
60+ ctx . emitter . updateAgentStatus ( stepUniqueAgentId , 'awaiting' ) ;
61+ }
62+
63+ // Handle Scenario 5: Fully autonomous prompt loop (interactive:false + autoMode + chainedPrompts)
64+ if ( ! ctx . mode . paused && behavior . runAutonomousLoop ) {
65+ debug ( '[Runner] Running autonomous prompt loop (Scenario 5)' ) ;
66+ await runAutonomousPromptLoop ( ctx ) ;
67+ return ;
68+ }
69+
70+ // Handle Scenario 6: Auto-advance (interactive:false + autoMode + no chainedPrompts)
71+ // This can happen when queue is exhausted after autonomous loop
72+ if ( ! ctx . mode . paused && ! behavior . shouldWait && ! behavior . runAutonomousLoop ) {
73+ debug ( '[Runner] Auto-advancing to next step (Scenario 6)' ) ;
74+ if ( session ) {
75+ await session . complete ( ) ;
5776 }
77+ ctx . emitter . updateAgentStatus ( stepUniqueAgentId , 'completed' ) ;
78+ ctx . indexManager . resetQueue ( ) ;
79+ await ctx . indexManager . stepCompleted ( machineCtx . currentStepIndex ) ;
80+ ctx . machine . send ( { type : 'INPUT_RECEIVED' , input : '' } ) ;
81+ return ;
5882 }
5983
60- // Build input context
61- const step = ctx . moduleSteps [ machineCtx . currentStepIndex ] ;
62- const stepUniqueAgentId = getUniqueAgentId ( step , machineCtx . currentStepIndex ) ;
84+ // Get provider from WorkflowMode (single source of truth)
85+ // WorkflowMode.getActiveProvider() automatically handles paused and autoMode state
86+ const provider = ctx . mode . getActiveProvider ( ) ;
87+ if ( ctx . mode . paused ) {
88+ debug ( '[Runner] Workflow is paused, using user input provider' ) ;
89+ } else if ( ! ctx . mode . autoMode ) {
90+ debug ( '[Runner] Manual mode, using user input provider' ) ;
91+ }
6392
6493 // Get queue state from session if available, otherwise from indexManager
6594 const queueState = session
@@ -131,6 +160,76 @@ export async function handleWaiting(ctx: RunnerContext, callbacks: WaitCallbacks
131160 }
132161}
133162
163+ /**
164+ * Run autonomous prompt loop (Scenario 5)
165+ *
166+ * Automatically sends the next chained prompt without controller/user involvement.
167+ * Each prompt runs through the state machine naturally - when it completes,
168+ * handleWaiting is called again and this function sends the next prompt.
169+ *
170+ * Used when interactive:false + autoMode + hasChainedPrompts.
171+ */
172+ async function runAutonomousPromptLoop ( ctx : RunnerContext ) : Promise < void > {
173+ const machineCtx = ctx . machine . context ;
174+ const stepIndex = machineCtx . currentStepIndex ;
175+ const step = ctx . moduleSteps [ stepIndex ] ;
176+ const uniqueAgentId = getUniqueAgentId ( step , stepIndex ) ;
177+ const session = ctx . getCurrentSession ( ) ;
178+
179+ // Check if queue is exhausted
180+ const isExhausted = session
181+ ? session . isQueueExhausted
182+ : ctx . indexManager . isQueueExhausted ( ) ;
183+
184+ if ( isExhausted ) {
185+ // All prompts sent - complete step and advance to next
186+ debug ( '[Runner:autonomous] Queue exhausted, completing step %d' , stepIndex ) ;
187+ ctx . emitter . updateAgentStatus ( uniqueAgentId , 'completed' ) ;
188+ ctx . indexManager . resetQueue ( ) ;
189+ await ctx . indexManager . stepCompleted ( stepIndex ) ;
190+ ctx . machine . send ( { type : 'INPUT_RECEIVED' , input : '' } ) ;
191+ return ;
192+ }
193+
194+ // Get next prompt
195+ const nextPrompt = ctx . indexManager . getCurrentQueuedPrompt ( ) ;
196+ if ( ! nextPrompt ) {
197+ // No more prompts - complete step and advance
198+ debug ( '[Runner:autonomous] No more prompts, completing step %d' , stepIndex ) ;
199+ ctx . emitter . updateAgentStatus ( uniqueAgentId , 'completed' ) ;
200+ ctx . indexManager . resetQueue ( ) ;
201+ await ctx . indexManager . stepCompleted ( stepIndex ) ;
202+ ctx . machine . send ( { type : 'INPUT_RECEIVED' , input : '' } ) ;
203+ return ;
204+ }
205+
206+ // Send the next prompt
207+ const chainIndex = ctx . indexManager . promptQueueIndex ;
208+ debug ( '[Runner:autonomous] Sending prompt %d: %s...' , chainIndex , nextPrompt . content . slice ( 0 , 50 ) ) ;
209+
210+ // Advance queue
211+ if ( session ) {
212+ session . advanceQueue ( ) ;
213+ } else {
214+ ctx . indexManager . advanceQueue ( ) ;
215+ }
216+
217+ // Track chain completion
218+ await ctx . indexManager . chainCompleted ( stepIndex , chainIndex ) ;
219+
220+ // Resume step with the prompt - when it completes, state machine will
221+ // transition back to awaiting and handleWaiting will be called again
222+ ctx . machine . send ( { type : 'RESUME' } ) ;
223+ await runStepResume ( ctx , {
224+ resumePrompt : nextPrompt . content ,
225+ resumeMonitoringId : machineCtx . currentMonitoringId ,
226+ source : 'controller' ,
227+ } ) ;
228+ // After runStepResume completes, machine goes back to awaiting state
229+ // and handleWaiting will be called again - it will detect Scenario 5
230+ // and call this function again to send the next prompt
231+ }
232+
134233/**
135234 * Handle resume with input - delegates to step/run.ts
136235 */
0 commit comments