Skip to content

Commit d1fac7b

Browse files
committed
feat(workflows): implement pause directive handling and UI integration
add pause directive handler similar to checkpoint pattern update workflow runner to handle pause in various scenarios preload chained prompts for immediate UI display remove fallback step info display in output window
1 parent 75dd36f commit d1fac7b

File tree

9 files changed

+239
-22
lines changed

9 files changed

+239
-22
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
<step n="1" goal="Check current state">
22
<action>Read `.codemachine/memory/directive.json` to see current directive state</action>
33
<action>Report what you found</action>
4+
<action>Write to `.codemachine/memory/directive.json`:
5+
```json
6+
{
7+
"action": "pause",
8+
"reason": "Testing pause directive"
9+
}
10+
```
11+
</action>
412
</step>

src/cli/tui/routes/workflow/components/output/output-window.tsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,6 @@ export function OutputWindow(props: OutputWindowProps) {
134134
},
135135
}
136136
}
137-
// Fallback: use workflow step info from currentAgent
138-
const agent = props.currentAgent
139-
if (agent?.stepIndex !== undefined && agent?.totalSteps !== undefined && agent.totalSteps > 1) {
140-
return {
141-
mode: "passive",
142-
chainedStep: {
143-
name: agent.name,
144-
index: agent.stepIndex + 1,
145-
total: agent.totalSteps,
146-
},
147-
}
148-
}
149137
return { mode: "passive" }
150138
}
151139

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Pause Directive Handler
3+
*
4+
* Handles agent-written { action: 'pause' } directive.
5+
* Similar pattern to checkpoint/handler.ts.
6+
*/
7+
8+
import type { WorkflowStep } from '../../templates/index.js';
9+
import { isModuleStep } from '../../templates/types.js';
10+
import { evaluatePauseDirective } from './evaluator.js';
11+
import type { WorkflowEventEmitter } from '../../events/emitter.js';
12+
13+
export interface PauseHandlerDecision {
14+
shouldPause: boolean;
15+
reason?: string;
16+
}
17+
18+
export async function handlePauseLogic(
19+
step: WorkflowStep,
20+
cwd: string,
21+
emitter?: WorkflowEventEmitter,
22+
): Promise<PauseHandlerDecision | null> {
23+
// Only module steps can have pause behavior
24+
if (!isModuleStep(step)) {
25+
return null;
26+
}
27+
28+
const pauseDecision = await evaluatePauseDirective({
29+
cwd,
30+
pauseRequested: false, // Only check agent-written directive, not user keypress
31+
});
32+
33+
if (pauseDecision?.shouldPause) {
34+
const message = `${step.agentName} requested pause` +
35+
`${pauseDecision.reason ? `: ${pauseDecision.reason}` : ''}`;
36+
37+
emitter?.logMessage(step.agentId, message);
38+
39+
return {
40+
shouldPause: true,
41+
reason: pauseDecision.reason,
42+
};
43+
}
44+
45+
return null;
46+
}

src/workflows/directives/pause/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77

88
export * from './types.js';
99
export * from './evaluator.js';
10+
export * from './handler.js';

src/workflows/runner/delegated.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,19 @@ export async function handleDelegated(ctx: RunnerContext, callbacks: DelegatedCa
5353
// Update agent status to delegated
5454
ctx.emitter.updateAgentStatus(stepUniqueAgentId, 'delegated');
5555

56+
// Emit input state so UI shows chained prompts info in delegated mode
57+
const queueStateForUI = session
58+
? session.getQueueState()
59+
: { promptQueue: [...ctx.indexManager.promptQueue], promptQueueIndex: ctx.indexManager.promptQueueIndex };
60+
if (queueStateForUI.promptQueue.length > 0) {
61+
ctx.emitter.setInputState({
62+
active: false, // Not active - controller is running
63+
queuedPrompts: queueStateForUI.promptQueue.map(p => ({ name: p.name, label: p.label, content: p.content })),
64+
currentIndex: queueStateForUI.promptQueueIndex,
65+
monitoringId: machineCtx.currentMonitoringId,
66+
});
67+
}
68+
5669
// Resolve interactive behavior using actual mode state
5770
const behavior = resolveInteractiveBehavior({
5871
step,

src/workflows/runner/wait.ts

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,64 @@ export async function handleWaiting(ctx: RunnerContext, callbacks: WaitCallbacks
7272
// Handle Scenario 6: Auto-advance (interactive:false + autoMode + no chainedPrompts)
7373
// This can happen when queue is exhausted after autonomous loop
7474
if (!ctx.mode.paused && !behavior.shouldWait && !behavior.runAutonomousLoop) {
75-
debug('[Runner] Auto-advancing to next step (Scenario 6)');
76-
if (session) {
77-
await session.complete();
75+
const action = await processPostStepDirectives({
76+
ctx,
77+
step,
78+
stepOutput: { output: machineCtx.currentOutput?.output ?? '' },
79+
stepIndex: machineCtx.currentStepIndex,
80+
uniqueAgentId: stepUniqueAgentId,
81+
});
82+
83+
debug('[Runner:scenario6] Post-step action: %s', action.type);
84+
85+
switch (action.type) {
86+
case 'stop':
87+
ctx.machine.send({ type: 'STOP' });
88+
return;
89+
90+
case 'checkpoint':
91+
return;
92+
93+
case 'pause': {
94+
debug('[Runner:scenario6] Directive: pause');
95+
ctx.emitter.logMessage(stepUniqueAgentId, `Paused${action.reason ? `: ${action.reason}` : ''}`);
96+
ctx.mode.pause();
97+
machineCtx.paused = true;
98+
ctx.emitter.updateAgentStatus(stepUniqueAgentId, 'awaiting');
99+
// Enable input box for user to resume
100+
const pauseQueueState = session
101+
? session.getQueueState()
102+
: { promptQueue: [...ctx.indexManager.promptQueue], promptQueueIndex: ctx.indexManager.promptQueueIndex };
103+
ctx.emitter.setInputState({
104+
active: true,
105+
queuedPrompts: pauseQueueState.promptQueue.map(p => ({ name: p.name, label: p.label, content: p.content })),
106+
currentIndex: pauseQueueState.promptQueueIndex,
107+
monitoringId: machineCtx.currentMonitoringId,
108+
});
109+
return;
110+
}
111+
112+
case 'loop':
113+
debug('[Runner:scenario6] Loop to step %d', action.targetIndex);
114+
ctx.emitter.updateAgentStatus(stepUniqueAgentId, 'completed');
115+
await ctx.indexManager.stepCompleted(machineCtx.currentStepIndex);
116+
ctx.indexManager.resetQueue();
117+
machineCtx.currentStepIndex = action.targetIndex;
118+
ctx.machine.send({ type: 'INPUT_RECEIVED', input: '' });
119+
return;
120+
121+
case 'advance':
122+
default:
123+
debug('[Runner] Auto-advancing to next step (Scenario 6)');
124+
if (session) {
125+
await session.complete();
126+
}
127+
ctx.emitter.updateAgentStatus(stepUniqueAgentId, 'completed');
128+
ctx.indexManager.resetQueue();
129+
await ctx.indexManager.stepCompleted(machineCtx.currentStepIndex);
130+
ctx.machine.send({ type: 'INPUT_RECEIVED', input: '' });
131+
return;
78132
}
79-
ctx.emitter.updateAgentStatus(stepUniqueAgentId, 'completed');
80-
ctx.indexManager.resetQueue();
81-
await ctx.indexManager.stepCompleted(machineCtx.currentStepIndex);
82-
ctx.machine.send({ type: 'INPUT_RECEIVED', input: '' });
83-
return;
84133
}
85134

86135
// Get provider from WorkflowMode (single source of truth)
@@ -242,6 +291,27 @@ async function runAutonomousPromptLoop(ctx: RunnerContext): Promise<void> {
242291
const uniqueAgentId = getUniqueAgentId(step, stepIndex);
243292
const session = ctx.getCurrentSession();
244293

294+
// Check for pause directive BEFORE sending next prompt (regardless of queue state)
295+
const pauseResult = await evaluateOnAdvance(ctx.cwd);
296+
if (pauseResult.type === 'pause') {
297+
debug('[Runner:autonomous] Directive: pause (before next prompt)');
298+
ctx.emitter.logMessage(uniqueAgentId, `Paused${pauseResult.reason ? `: ${pauseResult.reason}` : ''}`);
299+
ctx.mode.pause();
300+
machineCtx.paused = true;
301+
ctx.emitter.updateAgentStatus(uniqueAgentId, 'awaiting');
302+
// Enable input box for user to resume
303+
const queueState = session
304+
? session.getQueueState()
305+
: { promptQueue: [...ctx.indexManager.promptQueue], promptQueueIndex: ctx.indexManager.promptQueueIndex };
306+
ctx.emitter.setInputState({
307+
active: true,
308+
queuedPrompts: queueState.promptQueue.map(p => ({ name: p.name, label: p.label, content: p.content })),
309+
currentIndex: queueState.promptQueueIndex,
310+
monitoringId: machineCtx.currentMonitoringId,
311+
});
312+
return;
313+
}
314+
245315
// Check if queue is exhausted
246316
const isExhausted = session
247317
? session.isQueueExhausted
@@ -268,6 +338,25 @@ async function runAutonomousPromptLoop(ctx: RunnerContext): Promise<void> {
268338
// Checkpoint was handled by afterRun, just stay in current state
269339
return;
270340

341+
case 'pause': {
342+
debug('[Runner:autonomous] Directive: pause');
343+
ctx.emitter.logMessage(uniqueAgentId, `Paused${action.reason ? `: ${action.reason}` : ''}`);
344+
ctx.mode.pause();
345+
machineCtx.paused = true;
346+
ctx.emitter.updateAgentStatus(uniqueAgentId, 'awaiting');
347+
// Enable input box for user to resume
348+
const pauseQueueState = session
349+
? session.getQueueState()
350+
: { promptQueue: [...ctx.indexManager.promptQueue], promptQueueIndex: ctx.indexManager.promptQueueIndex };
351+
ctx.emitter.setInputState({
352+
active: true,
353+
queuedPrompts: pauseQueueState.promptQueue.map(p => ({ name: p.name, label: p.label, content: p.content })),
354+
currentIndex: pauseQueueState.promptQueueIndex,
355+
monitoringId: machineCtx.currentMonitoringId,
356+
});
357+
return;
358+
}
359+
271360
case 'loop':
272361
// Loop directive processed - rewind to target step
273362
debug('[Runner:autonomous] Loop to step %d', action.targetIndex);
@@ -311,6 +400,25 @@ async function runAutonomousPromptLoop(ctx: RunnerContext): Promise<void> {
311400
case 'checkpoint':
312401
return;
313402

403+
case 'pause': {
404+
debug('[Runner:autonomous] Directive: pause (no prompts)');
405+
ctx.emitter.logMessage(uniqueAgentId, `Paused${action.reason ? `: ${action.reason}` : ''}`);
406+
ctx.mode.pause();
407+
machineCtx.paused = true;
408+
ctx.emitter.updateAgentStatus(uniqueAgentId, 'awaiting');
409+
// Enable input box for user to resume
410+
const pauseQueueState = session
411+
? session.getQueueState()
412+
: { promptQueue: [...ctx.indexManager.promptQueue], promptQueueIndex: ctx.indexManager.promptQueueIndex };
413+
ctx.emitter.setInputState({
414+
active: true,
415+
queuedPrompts: pauseQueueState.promptQueue.map(p => ({ name: p.name, label: p.label, content: p.content })),
416+
currentIndex: pauseQueueState.promptQueueIndex,
417+
monitoringId: machineCtx.currentMonitoringId,
418+
});
419+
return;
420+
}
421+
314422
case 'loop':
315423
debug('[Runner:autonomous] Loop to step %d', action.targetIndex);
316424
ctx.emitter.updateAgentStatus(uniqueAgentId, 'completed');

src/workflows/step/hooks.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { StepIndexManager } from '../indexing/index.js';
1414
import { handleTriggerLogic } from '../directives/trigger/index.js';
1515
import { handleCheckpointLogic } from '../directives/checkpoint/index.js';
1616
import { handleErrorLogic } from '../directives/error/index.js';
17+
import { handlePauseLogic } from '../directives/pause/index.js';
1718
import type { WorkflowEventEmitter } from '../events/index.js';
1819
import { type ModuleStep, type WorkflowTemplate, isModuleStep } from '../templates/types.js';
1920
import { getUniqueAgentId } from '../context/index.js';
@@ -126,6 +127,8 @@ export interface AfterRunResult {
126127
stoppedByCheckpointQuit?: boolean;
127128
workflowShouldStop?: boolean;
128129
checkpointContinued?: boolean;
130+
pauseRequested?: boolean;
131+
pauseReason?: string;
129132
}
130133

131134
/**
@@ -153,7 +156,8 @@ export type PostStepAction =
153156
| { type: 'advance' }
154157
| { type: 'loop'; targetIndex: number; newActiveLoop: ActiveLoop | null }
155158
| { type: 'stop' }
156-
| { type: 'checkpoint' };
159+
| { type: 'checkpoint' }
160+
| { type: 'pause'; reason?: string };
157161

158162
/**
159163
* Context for processPostStepDirectives
@@ -203,6 +207,10 @@ export async function processPostStepDirectives(context: ProcessPostStepContext)
203207
return { type: 'checkpoint' };
204208
}
205209

210+
if (postResult.pauseRequested) {
211+
return { type: 'pause', reason: postResult.pauseReason };
212+
}
213+
206214
// Handle loop - newIndex is the raw index from loop logic
207215
if (postResult.newIndex !== undefined) {
208216
// Update active loop state
@@ -304,6 +312,12 @@ export async function afterRun(options: AfterRunOptions): Promise<AfterRunResult
304312
return { shouldBreak: false, checkpointContinued: true };
305313
}
306314

315+
// Check for pause directive
316+
const pauseResult = await handlePauseLogic(step, cwd, emitter);
317+
if (pauseResult?.shouldPause) {
318+
return { shouldBreak: false, pauseRequested: true, pauseReason: pauseResult.reason };
319+
}
320+
307321
// Check for loop directive
308322
const loopResult = await handleLoopLogic(step, index, stepOutput.output, loopCounters, cwd, emitter);
309323

src/workflows/step/run.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,33 @@ export async function runStepFresh(ctx: RunnerContext): Promise<RunStepResult |
134134
ctx.emitter.updateAgentModel(uniqueAgentId, resolvedModel);
135135
}
136136

137+
// Pre-load chained prompts from agent config so UI can show step info immediately
138+
const agentConfig = await loadAgentConfig(step.agentId, ctx.cwd);
139+
if (agentConfig?.chainedPromptsPath) {
140+
const selectedConditions = await getSelectedConditions(ctx.cmRoot);
141+
const preloadedPrompts = await loadChainedPrompts(
142+
agentConfig.chainedPromptsPath,
143+
ctx.cwd,
144+
selectedConditions
145+
);
146+
if (preloadedPrompts.length > 0) {
147+
debug('[step/run] Pre-loaded %d chained prompts for UI', preloadedPrompts.length);
148+
const session = ctx.getCurrentSession();
149+
if (session) {
150+
session.loadChainedPrompts(preloadedPrompts);
151+
} else {
152+
ctx.indexManager.initQueue(preloadedPrompts, 0);
153+
}
154+
// Emit input state immediately so UI shows chained prompts info
155+
ctx.emitter.setInputState({
156+
active: false, // Not active yet - agent is running
157+
queuedPrompts: preloadedPrompts.map(p => ({ name: p.name, label: p.label, content: p.content })),
158+
currentIndex: 0,
159+
monitoringId: undefined, // Not yet known
160+
});
161+
}
162+
}
163+
137164
try {
138165
// Execute the step
139166
const output = await executeStep(step, ctx.cwd, {
@@ -248,6 +275,18 @@ export async function runStepFresh(ctx: RunnerContext): Promise<RunStepResult |
248275
if (!machineCtx.autoMode) {
249276
ctx.emitter.updateAgentStatus(uniqueAgentId, 'awaiting');
250277
}
278+
279+
// Emit input state for fresh start so UI shows chained prompts info immediately
280+
const queueState = session
281+
? session.getQueueState()
282+
: { promptQueue: [...ctx.indexManager.promptQueue], promptQueueIndex: ctx.indexManager.promptQueueIndex };
283+
ctx.emitter.setInputState({
284+
active: !machineCtx.autoMode, // Only active input box in manual mode
285+
queuedPrompts: queueState.promptQueue.map(p => ({ name: p.name, label: p.label, content: p.content })),
286+
currentIndex: queueState.promptQueueIndex,
287+
monitoringId: stepOutput.monitoringId,
288+
});
289+
251290
ctx.machine.send({ type: 'STEP_COMPLETE', output: stepOutput });
252291
return { output: stepOutput };
253292
} else {

templates/workflows/test2.workflow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default {
1414
// Test loop directive respect in Scenario 6
1515
// This module has chained prompts that set loop directive
1616
// After chained prompts complete, Scenario 6 should respect the loop directive
17-
resolveModule('test-loop', { interactive: false, loopSteps: 1 }),
17+
resolveModule('test-loop', { interactive: false, loopSteps: 3 }),
1818
],
1919
subAgentIds: ['frontend-dev'],
2020
};

0 commit comments

Comments
 (0)