Skip to content

Commit a7baba5

Browse files
committed
feat(workflow): add auto mode continuation prompt handling
- Add DEFAULT_CONTINUATION_PROMPT constant for auto mode recovery and delegation - Track continuationPromptSent in workflow context to prevent duplicate prompts - Send continuation prompt when first entering delegated state in auto mode - Handle mode switches during prompt execution in delegated state
1 parent 55efca7 commit a7baba5

File tree

6 files changed

+118
-9
lines changed

6 files changed

+118
-9
lines changed

src/shared/prompts/continuation.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Continuation Prompts for Auto Mode
3+
*
4+
* Shared prompts used when continuing workflow execution in auto mode.
5+
*/
6+
7+
/**
8+
* Default continuation prompt for auto mode
9+
*
10+
* Used when:
11+
* 1. Crash recovery in auto mode - to resume agent where it left off
12+
* 2. Entering delegated state - to prompt agent before controller decides
13+
*/
14+
export const DEFAULT_CONTINUATION_PROMPT =
15+
'Continue where you left off. Review what was accomplished and proceed with the next logical step.';

src/shared/prompts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { processPrompt, processPromptString } from './replacement/processor.js';
2+
export { DEFAULT_CONTINUATION_PROMPT } from './continuation.js';

src/workflows/recovery/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { debug } from '../../shared/logging/logger.js';
9+
import { DEFAULT_CONTINUATION_PROMPT } from '../../shared/prompts/index.js';
910
import type { StepData } from '../indexing/types.js';
1011
import type { WorkflowEventEmitter } from '../events/index.js';
1112
import type { StepIndexManager } from '../indexing/index.js';
@@ -140,7 +141,6 @@ export async function handleCrashRecovery(
140141

141142
// 3. Handle recovery based on mode
142143
const isAutoMode = machine.context.autoMode;
143-
const recoveryPrompt = 'Continue where you left off. Review what was accomplished and proceed with the next logical step.';
144144

145145
if (isAutoMode) {
146146
// Auto mode: Send recovery prompt directly before transitioning
@@ -152,12 +152,15 @@ export async function handleCrashRecovery(
152152
debug('[recovery] Auto mode: sending recovery prompt to agent');
153153
emitter.updateAgentStatus(uniqueAgentId, 'running');
154154

155+
// Mark continuation prompt as sent to prevent handleDelegated from sending again
156+
machine.context.continuationPromptSent = true;
157+
155158
// Transition to running state before sending prompt
156159
machine.send({ type: 'RESUME' });
157160

158161
// Send recovery prompt and wait for agent response
159162
await options.sendRecoveryPrompt({
160-
resumePrompt: recoveryPrompt,
163+
resumePrompt: DEFAULT_CONTINUATION_PROMPT,
161164
resumeMonitoringId: stepData?.monitoringId,
162165
source: 'controller',
163166
});

src/workflows/runner/delegated.ts

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { debug } from '../../shared/logging/logger.js';
9+
import { DEFAULT_CONTINUATION_PROMPT } from '../../shared/prompts/index.js';
910
import type { InputContext } from '../input/index.js';
1011
import { getUniqueAgentId } from '../context/index.js';
1112
import { runStepResume } from '../step/run.js';
@@ -81,10 +82,61 @@ export async function handleDelegated(ctx: RunnerContext, callbacks: DelegatedCa
8182
debug('[Runner:delegated] Scenario=%d, shouldWait=%s, runAutonomousLoop=%s',
8283
behavior.scenario, behavior.shouldWait, behavior.runAutonomousLoop);
8384

85+
// Auto mode continuation: Send continuation prompt FIRST before any scenario handling
86+
// This ensures agent gets a chance to continue when auto mode is enabled
87+
if (!machineCtx.continuationPromptSent && machineCtx.currentOutput) {
88+
debug('[Runner:delegated] Sending continuation prompt (first entry into auto mode)');
89+
90+
// Mark as sent to prevent re-sending on next entry
91+
machineCtx.continuationPromptSent = true;
92+
93+
// Update status and transition to running
94+
ctx.emitter.updateAgentStatus(stepUniqueAgentId, 'running');
95+
ctx.machine.send({ type: 'RESUME' });
96+
97+
// Track mode switch during execution (so pause works mid-turn)
98+
let modeSwitchRequested: 'manual' | null = null;
99+
const modeChangeHandler = (data: { autonomousMode: boolean }) => {
100+
if (!data.autonomousMode) {
101+
debug('[Runner:delegated] Mode change to manual during continuation prompt');
102+
modeSwitchRequested = 'manual';
103+
ctx.getAbortController()?.abort();
104+
}
105+
};
106+
process.on('workflow:mode-change', modeChangeHandler);
107+
108+
try {
109+
// Send continuation prompt and wait for agent response
110+
await runStepResume(ctx, {
111+
resumePrompt: DEFAULT_CONTINUATION_PROMPT,
112+
resumeMonitoringId: machineCtx.currentMonitoringId,
113+
source: 'controller',
114+
});
115+
116+
debug('[Runner:delegated] Continuation prompt sent, agent responded');
117+
} catch (error) {
118+
if (error instanceof Error && error.name === 'AbortError') {
119+
if (modeSwitchRequested === 'manual') {
120+
debug('[Runner:delegated] Continuation prompt aborted due to mode switch to manual');
121+
ctx.emitter.updateAgentStatus(stepUniqueAgentId, 'awaiting');
122+
await callbacks.setAutoMode(false);
123+
}
124+
return;
125+
}
126+
throw error;
127+
} finally {
128+
process.removeListener('workflow:mode-change', modeChangeHandler);
129+
}
130+
131+
// After agent responds, state machine transitions back to delegated
132+
// Next call to handleDelegated will proceed with scenario handling
133+
return;
134+
}
135+
84136
// Handle Scenario 5: Fully autonomous prompt loop (interactive:false + autoMode + chainedPrompts)
85137
if (behavior.runAutonomousLoop) {
86138
debug('[Runner:delegated] Running autonomous prompt loop (Scenario 5)');
87-
await runAutonomousPromptLoop(ctx);
139+
await runAutonomousPromptLoop(ctx, callbacks);
88140
return;
89141
}
90142

@@ -263,7 +315,7 @@ export async function handleDelegated(ctx: RunnerContext, callbacks: DelegatedCa
263315
*
264316
* Automatically sends the next chained prompt without controller involvement.
265317
*/
266-
async function runAutonomousPromptLoop(ctx: RunnerContext): Promise<void> {
318+
async function runAutonomousPromptLoop(ctx: RunnerContext, callbacks: DelegatedCallbacks): Promise<void> {
267319
const machineCtx = ctx.machine.context;
268320
const stepIndex = machineCtx.currentStepIndex;
269321
const step = ctx.moduleSteps[stepIndex];
@@ -375,11 +427,37 @@ async function runAutonomousPromptLoop(ctx: RunnerContext): Promise<void> {
375427

376428
// Resume step with the prompt
377429
ctx.machine.send({ type: 'RESUME' });
378-
await runStepResume(ctx, {
379-
resumePrompt: nextPrompt.content,
380-
resumeMonitoringId: machineCtx.currentMonitoringId,
381-
source: 'controller',
382-
});
430+
431+
// Track mode switch during execution (so pause works mid-turn)
432+
let modeSwitchRequested: 'manual' | null = null;
433+
const modeChangeHandler = (data: { autonomousMode: boolean }) => {
434+
if (!data.autonomousMode) {
435+
debug('[Runner:delegated:autonomous] Mode change to manual during prompt execution');
436+
modeSwitchRequested = 'manual';
437+
ctx.getAbortController()?.abort();
438+
}
439+
};
440+
process.on('workflow:mode-change', modeChangeHandler);
441+
442+
try {
443+
await runStepResume(ctx, {
444+
resumePrompt: nextPrompt.content,
445+
resumeMonitoringId: machineCtx.currentMonitoringId,
446+
source: 'controller',
447+
});
448+
} catch (error) {
449+
if (error instanceof Error && error.name === 'AbortError') {
450+
if (modeSwitchRequested === 'manual') {
451+
debug('[Runner:delegated:autonomous] Prompt execution aborted due to mode switch to manual');
452+
ctx.emitter.updateAgentStatus(uniqueAgentId, 'awaiting');
453+
await callbacks.setAutoMode(false);
454+
}
455+
return;
456+
}
457+
throw error;
458+
} finally {
459+
process.removeListener('workflow:mode-change', modeChangeHandler);
460+
}
383461
}
384462

385463
/**

src/workflows/state/machine.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export function createWorkflowMachine(initialContext: Partial<WorkflowContext> =
127127
paused: false,
128128
cwd: process.cwd(),
129129
cmRoot: '',
130+
continuationPromptSent: false,
130131
...initialContext,
131132
};
132133

@@ -196,6 +197,7 @@ export function createWorkflowMachine(initialContext: Partial<WorkflowContext> =
196197
guard: (ctx) => ctx.currentStepIndex < ctx.totalSteps - 1,
197198
action: (ctx) => {
198199
ctx.currentStepIndex += 1;
200+
ctx.continuationPromptSent = false;
199201
debug('[FSM] Skipped during run, advancing to step %d', ctx.currentStepIndex + 1);
200202
},
201203
},
@@ -246,6 +248,7 @@ export function createWorkflowMachine(initialContext: Partial<WorkflowContext> =
246248
action: (ctx, event) => {
247249
if (event.type === 'INPUT_RECEIVED') {
248250
ctx.currentStepIndex += 1;
251+
ctx.continuationPromptSent = false;
249252
debug('[FSM] Input received, advancing to step %d', ctx.currentStepIndex + 1);
250253
}
251254
},
@@ -267,6 +270,7 @@ export function createWorkflowMachine(initialContext: Partial<WorkflowContext> =
267270
guard: (ctx) => ctx.currentStepIndex < ctx.totalSteps - 1,
268271
action: (ctx) => {
269272
ctx.currentStepIndex += 1;
273+
ctx.continuationPromptSent = false;
270274
debug('[FSM] Skipped, advancing to step %d', ctx.currentStepIndex + 1);
271275
},
272276
},
@@ -291,6 +295,7 @@ export function createWorkflowMachine(initialContext: Partial<WorkflowContext> =
291295
target: 'awaiting',
292296
action: (ctx) => {
293297
ctx.autoMode = false;
298+
ctx.continuationPromptSent = false; // Reset so next auto mode entry sends prompt
294299
debug('[FSM] Mode switched to manual, transitioning to awaiting');
295300
},
296301
},
@@ -301,6 +306,7 @@ export function createWorkflowMachine(initialContext: Partial<WorkflowContext> =
301306
action: (ctx, event) => {
302307
if (event.type === 'INPUT_RECEIVED') {
303308
ctx.currentStepIndex += 1;
309+
ctx.continuationPromptSent = false;
304310
debug('[FSM] Controller input received, advancing to step %d', ctx.currentStepIndex + 1);
305311
}
306312
},
@@ -319,6 +325,7 @@ export function createWorkflowMachine(initialContext: Partial<WorkflowContext> =
319325
guard: (ctx) => ctx.currentStepIndex < ctx.totalSteps - 1,
320326
action: (ctx) => {
321327
ctx.currentStepIndex += 1;
328+
ctx.continuationPromptSent = false;
322329
debug('[FSM] Controller skipped, advancing to step %d', ctx.currentStepIndex + 1);
323330
},
324331
},
@@ -332,6 +339,7 @@ export function createWorkflowMachine(initialContext: Partial<WorkflowContext> =
332339
action: (ctx) => {
333340
ctx.autoMode = false;
334341
ctx.paused = true;
342+
ctx.continuationPromptSent = false; // Reset so next auto mode entry sends prompt
335343
debug('[FSM] Controller paused - switching to manual mode');
336344
},
337345
},

src/workflows/state/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export interface WorkflowContext {
7070

7171
// Error tracking
7272
lastError?: Error;
73+
74+
// Auto mode continuation tracking
75+
// When true, continuation prompt has been sent for current step in delegated state
76+
continuationPromptSent?: boolean;
7377
}
7478

7579
/**

0 commit comments

Comments
 (0)