Skip to content

Commit 7cc1cfc

Browse files
committed
feat(behaviors): implement pause behavior with centralized manager
Add pause behavior module with evaluator, listener and handler components Introduce behavior manager to coordinate all workflow behaviors Update runner to use behavior manager for pause functionality
1 parent ba7b46a commit 7cc1cfc

File tree

11 files changed

+536
-44
lines changed

11 files changed

+536
-44
lines changed

src/workflows/behaviors/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ export * from './loop/index.js';
1111
export * from './trigger/index.js';
1212
export * from './checkpoint/index.js';
1313
export * from './error/index.js';
14+
export * from './manager/index.js';
15+
export * from './pause/index.js';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Behavior Manager Module
3+
*
4+
* Central coordinator between runner and individual behaviors.
5+
*/
6+
7+
export * from './types.js';
8+
export * from './manager.js';
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Behavior Manager
3+
*
4+
* Central coordinator for all workflow behaviors.
5+
* Runner only knows about the manager, not individual behaviors.
6+
*/
7+
8+
import { debug } from '../../../shared/logging/logger.js';
9+
import type { WorkflowEventEmitter } from '../../events/emitter.js';
10+
import type { StateMachine } from '../../state/index.js';
11+
import type {
12+
Behavior,
13+
BehaviorType,
14+
BehaviorContext,
15+
BehaviorResult,
16+
BehaviorManagerOptions,
17+
BehaviorInitContext,
18+
StepContext,
19+
} from './types.js';
20+
import { PauseBehavior } from '../pause/handler.js';
21+
22+
/**
23+
* BehaviorManager - orchestrates all workflow behaviors
24+
*/
25+
export class BehaviorManager {
26+
private behaviors: Map<BehaviorType, Behavior> = new Map();
27+
private emitter: WorkflowEventEmitter;
28+
private machine: StateMachine;
29+
private cwd: string;
30+
private cmRoot: string;
31+
private abortController: AbortController | null = null;
32+
private stepContext: StepContext | null = null;
33+
34+
constructor(options: BehaviorManagerOptions) {
35+
this.emitter = options.emitter;
36+
this.machine = options.machine;
37+
this.cwd = options.cwd;
38+
this.cmRoot = options.cmRoot;
39+
40+
// Register built-in behaviors
41+
this.registerBuiltInBehaviors();
42+
43+
// Initialize all behaviors
44+
this.initAll();
45+
}
46+
47+
/**
48+
* Register built-in behaviors
49+
*/
50+
private registerBuiltInBehaviors(): void {
51+
this.register(new PauseBehavior());
52+
// Future: this.register(new SkipBehavior());
53+
// Future: this.register(new StopBehavior());
54+
}
55+
56+
/**
57+
* Initialize all behaviors
58+
*/
59+
private initAll(): void {
60+
const initContext: BehaviorInitContext = {
61+
getAbortController: () => this.abortController,
62+
getStepContext: () => this.stepContext,
63+
emitter: this.emitter,
64+
machine: this.machine,
65+
cwd: this.cwd,
66+
cmRoot: this.cmRoot,
67+
};
68+
69+
for (const behavior of this.behaviors.values()) {
70+
behavior.init?.(initContext);
71+
}
72+
}
73+
74+
/**
75+
* Register a behavior
76+
*/
77+
register(behavior: Behavior): void {
78+
debug('[BehaviorManager] Registering behavior: %s', behavior.name);
79+
this.behaviors.set(behavior.name, behavior);
80+
}
81+
82+
/**
83+
* Get a registered behavior by type
84+
*/
85+
get<T extends Behavior>(type: BehaviorType): T | undefined {
86+
return this.behaviors.get(type) as T | undefined;
87+
}
88+
89+
/**
90+
* Set the current abort controller (called by runner)
91+
*/
92+
setAbortController(controller: AbortController | null): void {
93+
this.abortController = controller;
94+
}
95+
96+
/**
97+
* Get the current abort controller
98+
*/
99+
getAbortController(): AbortController | null {
100+
return this.abortController;
101+
}
102+
103+
/**
104+
* Set current step context (called by runner before each step)
105+
*/
106+
setStepContext(context: StepContext | null): void {
107+
this.stepContext = context;
108+
}
109+
110+
/**
111+
* Get current step context
112+
*/
113+
getStepContext(): StepContext | null {
114+
return this.stepContext;
115+
}
116+
117+
/**
118+
* Handle a behavior event
119+
*/
120+
async handle(
121+
type: BehaviorType,
122+
partialContext: Partial<BehaviorContext> & { stepIndex: number; agentId: string; agentName: string }
123+
): Promise<BehaviorResult> {
124+
const behavior = this.behaviors.get(type);
125+
126+
if (!behavior) {
127+
debug('[BehaviorManager] No behavior registered for: %s', type);
128+
return { handled: false };
129+
}
130+
131+
const context: BehaviorContext = {
132+
cwd: this.cwd,
133+
cmRoot: this.cmRoot,
134+
emitter: this.emitter,
135+
machine: this.machine,
136+
abortController: this.abortController,
137+
...partialContext,
138+
};
139+
140+
debug('[BehaviorManager] Handling behavior: %s', type);
141+
return behavior.handle(context);
142+
}
143+
144+
/**
145+
* Reset all behaviors (called at step start)
146+
*/
147+
resetAll(): void {
148+
debug('[BehaviorManager] Resetting all behaviors');
149+
for (const behavior of this.behaviors.values()) {
150+
behavior.reset?.();
151+
}
152+
}
153+
154+
/**
155+
* Cleanup all behaviors (called on workflow end)
156+
*/
157+
cleanup(): void {
158+
debug('[BehaviorManager] Cleaning up all behaviors');
159+
for (const behavior of this.behaviors.values()) {
160+
behavior.cleanup?.();
161+
}
162+
this.behaviors.clear();
163+
}
164+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Behavior Manager Types
3+
*
4+
* Core interfaces for the behavior system.
5+
*/
6+
7+
import type { WorkflowEventEmitter } from '../../events/emitter.js';
8+
import type { StateMachine } from '../../state/index.js';
9+
10+
/**
11+
* Supported behavior types
12+
*/
13+
export type BehaviorType = 'pause' | 'skip' | 'stop' | 'mode-change';
14+
15+
/**
16+
* Current step context - set by runner before each step
17+
*/
18+
export interface StepContext {
19+
stepIndex: number;
20+
agentId: string;
21+
agentName: string;
22+
}
23+
24+
/**
25+
* Context passed to behaviors during initialization
26+
*/
27+
export interface BehaviorInitContext {
28+
getAbortController: () => AbortController | null;
29+
getStepContext: () => StepContext | null;
30+
emitter: WorkflowEventEmitter;
31+
machine: StateMachine;
32+
cwd: string;
33+
cmRoot: string;
34+
}
35+
36+
/**
37+
* Context passed to behaviors when handling events
38+
*/
39+
export interface BehaviorContext {
40+
cwd: string;
41+
cmRoot: string;
42+
stepIndex: number;
43+
agentId: string;
44+
agentName: string;
45+
abortController: AbortController | null;
46+
emitter: WorkflowEventEmitter;
47+
machine: StateMachine;
48+
}
49+
50+
/**
51+
* Result returned from behavior handlers
52+
*/
53+
export interface BehaviorResult {
54+
handled: boolean;
55+
action?: 'pause' | 'skip' | 'stop' | 'continue';
56+
reason?: string;
57+
}
58+
59+
/**
60+
* Behavior interface - all behaviors must implement this
61+
*/
62+
export interface Behavior {
63+
/** Unique name for this behavior */
64+
readonly name: BehaviorType;
65+
66+
/** Initialize behavior (setup listeners, etc.) */
67+
init?(context: BehaviorInitContext): void;
68+
69+
/** Check if behavior is active/triggered */
70+
isActive?(): boolean;
71+
72+
/** Trigger/request the behavior */
73+
trigger?(): void;
74+
75+
/** Handle the behavior event */
76+
handle(context: BehaviorContext): Promise<BehaviorResult>;
77+
78+
/** Reset behavior state (called at step start) */
79+
reset?(): void;
80+
81+
/** Cleanup resources (called on workflow end) */
82+
cleanup?(): void;
83+
}
84+
85+
/**
86+
* Options for creating a BehaviorManager
87+
*/
88+
export interface BehaviorManagerOptions {
89+
emitter: WorkflowEventEmitter;
90+
machine: StateMachine;
91+
cwd: string;
92+
cmRoot: string;
93+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Pause Behavior Evaluator
3+
*
4+
* Evaluates whether a pause should occur based on:
5+
* - User keypress (pauseRequested flag)
6+
* - Agent writing { action: 'pause' } to behavior.json
7+
*/
8+
9+
import { readBehaviorFile } from '../reader.js';
10+
import type { PauseDecision, PauseEvaluationOptions } from './types.js';
11+
12+
/**
13+
* Evaluate if pause behavior should trigger
14+
*/
15+
export async function evaluatePauseBehavior(
16+
options: PauseEvaluationOptions
17+
): Promise<PauseDecision | null> {
18+
const { cwd, pauseRequested } = options;
19+
20+
// Check user keypress first (higher priority)
21+
if (pauseRequested) {
22+
return { shouldPause: true, source: 'user' };
23+
}
24+
25+
// Check agent-written behavior.json
26+
const behaviorAction = await readBehaviorFile(cwd);
27+
if (behaviorAction?.action === 'pause') {
28+
return {
29+
shouldPause: true,
30+
reason: behaviorAction.reason,
31+
source: 'agent',
32+
};
33+
}
34+
35+
return null;
36+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Pause Behavior Handler
3+
*
4+
* Implements the Behavior interface for pause functionality.
5+
* The listener handles everything - log, state machine, abort.
6+
*/
7+
8+
import { debug } from '../../../shared/logging/logger.js';
9+
import type {
10+
Behavior,
11+
BehaviorContext,
12+
BehaviorResult,
13+
BehaviorInitContext,
14+
} from '../manager/types.js';
15+
import { createPauseListener } from './listener.js';
16+
17+
/**
18+
* PauseBehavior - handles workflow pause events
19+
*
20+
* When user presses 'p', the listener handles everything:
21+
* - Logs message to emitter
22+
* - Sends PAUSE to state machine
23+
* - Aborts current execution
24+
*
25+
* Runner just catches AbortError and returns.
26+
*/
27+
export class PauseBehavior implements Behavior {
28+
readonly name = 'pause' as const;
29+
private cleanupFn: (() => void) | null = null;
30+
31+
/**
32+
* Initialize behavior - setup event listener that handles everything
33+
*/
34+
init(context: BehaviorInitContext): void {
35+
debug('[PauseBehavior] Initializing');
36+
37+
this.cleanupFn = createPauseListener({
38+
getAbortController: context.getAbortController,
39+
getStepContext: context.getStepContext,
40+
emitter: context.emitter,
41+
machine: context.machine,
42+
});
43+
}
44+
45+
/**
46+
* Check if behavior is active (not used in new architecture)
47+
*/
48+
isActive(): boolean {
49+
return false; // Listener handles everything, no need to check
50+
}
51+
52+
/**
53+
* Trigger pause (for programmatic pause via runner.pause())
54+
*/
55+
trigger(): void {
56+
// Emit the event, listener will handle it
57+
process.emit('workflow:pause' as any);
58+
}
59+
60+
/**
61+
* Reset behavior state (called at step start)
62+
*/
63+
reset(): void {
64+
// Nothing to reset - listener handles everything immediately
65+
}
66+
67+
/**
68+
* Cleanup resources
69+
*/
70+
cleanup(): void {
71+
debug('[PauseBehavior] Cleaning up');
72+
this.cleanupFn?.();
73+
this.cleanupFn = null;
74+
}
75+
76+
/**
77+
* Handle pause behavior (legacy - not called in new architecture)
78+
* Kept for interface compliance
79+
*/
80+
async handle(_context: BehaviorContext): Promise<BehaviorResult> {
81+
// In new architecture, listener handles everything
82+
// This is only here for interface compliance
83+
return { handled: true, action: 'pause' };
84+
}
85+
}

0 commit comments

Comments
 (0)