Skip to content

Commit 274d3cf

Browse files
committed
feat(conditions): add support for OR conditions in workflow steps and chained prompts
implement conditionsAny field to allow steps to be executed when any of the specified conditions are met update condition checking logic in workflow runner and chained prompts loader to handle both AND and OR conditions
1 parent c3f698d commit 274d3cf

7 files changed

Lines changed: 35 additions & 5 deletions

File tree

src/agents/runner/chained.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,18 @@ function isConditionalPath(entry: ChainedPathEntry): entry is ConditionalChained
233233
*/
234234
function meetsConditions(entry: ChainedPathEntry, selectedConditions: string[]): boolean {
235235
if (typeof entry === 'string') return true;
236-
if (!entry.conditions?.length) return true;
237-
return entry.conditions.every(c => selectedConditions.includes(c));
236+
const requiredAll = entry.conditions ?? [];
237+
const requiredAny = entry.conditionsAny ?? [];
238+
239+
if (requiredAll.length > 0 && !requiredAll.every(c => selectedConditions.includes(c))) {
240+
return false;
241+
}
242+
243+
if (requiredAny.length > 0 && !requiredAny.some(c => selectedConditions.includes(c))) {
244+
return false;
245+
}
246+
247+
return true;
238248
}
239249

240250
/**
@@ -279,8 +289,11 @@ export async function loadChainedPrompts(
279289
// Check both conditions AND tracks (both must pass)
280290
if (!meetsConditions(entry, selectedConditions)) {
281291
const pathStr = getPath(entry);
282-
const conditions = isConditionalPath(entry) ? entry.conditions : [];
283-
debug(`Skipped chained path: ${pathStr} (unmet conditions: ${conditions?.join(', ')})`);
292+
const conditionsAll = isConditionalPath(entry) ? entry.conditions : [];
293+
const conditionsAny = isConditionalPath(entry) ? entry.conditionsAny : [];
294+
debug(
295+
`Skipped chained path: ${pathStr} (unmet conditions: all=${conditionsAll?.join(', ') || 'n/a'}, any=${conditionsAny?.join(', ') || 'n/a'})`
296+
);
284297
continue;
285298
}
286299

src/shared/agents/config/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export type ConditionalChainedPath = {
55
path: string;
66
conditions?: string[];
7+
conditionsAny?: string[];
78
tracks?: string[];
89
};
910

@@ -20,6 +21,8 @@ export type AgentDefinition = {
2021
engine?: string; // Engine to use for this agent (dynamically determined from registry)
2122
chainedPromptsPath?: ChainedPathEntry | ChainedPathEntry[]; // Path(s) to folder(s) containing chained prompt .md files
2223
role?: 'controller'; // Agent role - 'controller' agents can drive autonomous mode
24+
conditions?: string[];
25+
conditionsAny?: string[];
2326
[key: string]: unknown;
2427
};
2528

src/workflows/run.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,23 @@ export async function runWorkflow(options: RunWorkflowOptions = {}): Promise<voi
209209
idx, step.agentId, step.tracks, selectedTrack);
210210
return false;
211211
}
212+
const selected = selectedConditions ?? [];
212213
if (step.conditions?.length) {
213-
const missing = step.conditions.filter(c => !(selectedConditions ?? []).includes(c));
214+
const missing = step.conditions.filter(c => !selected.includes(c));
214215
if (missing.length > 0) {
215216
debug('[Workflow] Step %d: agentId=%s, conditions=%O, missing=%O → EXCLUDED (missing conditions)',
216217
idx, step.agentId, step.conditions, missing);
217218
return false;
218219
}
219220
}
221+
if (step.conditionsAny?.length) {
222+
const matched = step.conditionsAny.some(c => selected.includes(c));
223+
if (!matched) {
224+
debug('[Workflow] Step %d: agentId=%s, conditionsAny=%O → EXCLUDED (no match)',
225+
idx, step.agentId, step.conditionsAny);
226+
return false;
227+
}
228+
}
220229
debug('[Workflow] Step %d: agentId=%s → included', idx, step.agentId);
221230
return true;
222231
});

src/workflows/templates/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface ModuleStep {
4242
interactive?: boolean; // Controls waiting behavior: true=wait for input, false=auto-advance
4343
tracks?: string[]; // Track names this step belongs to (e.g., ['bmad', 'enterprise'])
4444
conditions?: string[]; // Conditions required for this step (e.g., ['has_ui', 'has_api'])
45+
conditionsAny?: string[]; // OR conditions for this step (e.g., ['full-workflow', 'workflow-definition'])
4546
}
4647

4748
export interface Separator {

src/workflows/utils/resolvers/module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export function resolveModule(id: string, overrides: ModuleOverrides = {}): Work
6868
interactive: overrides.interactive,
6969
tracks: overrides.tracks ?? moduleEntry.tracks as string[] | undefined,
7070
conditions: overrides.conditions ?? moduleEntry.conditions as string[] | undefined,
71+
conditionsAny: overrides.conditionsAny ?? moduleEntry.conditionsAny as string[] | undefined,
7172
module: {
7273
id: moduleEntry.id,
7374
behavior,

src/workflows/utils/resolvers/step.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,6 @@ export function resolveStep(id: string, overrides: StepOverrides = {}): Workflow
3333
interactive: overrides.interactive,
3434
tracks: overrides.tracks ?? agent.tracks as string[] | undefined,
3535
conditions: overrides.conditions ?? agent.conditions as string[] | undefined,
36+
conditionsAny: overrides.conditionsAny ?? agent.conditionsAny as string[] | undefined,
3637
};
3738
}

src/workflows/utils/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface StepOverrides {
88
interactive?: boolean; // Controls waiting behavior: true=wait for input, false=auto-advance
99
tracks?: string[]; // Track names this step belongs to (e.g., ['bmad', 'enterprise'])
1010
conditions?: string[]; // Conditions required for this step (e.g., ['has_ui', 'has_api'])
11+
conditionsAny?: string[]; // OR conditions for this step (e.g., ['full-workflow', 'workflow-definition'])
1112
}
1213

1314
export interface WorkflowStep {
@@ -23,6 +24,7 @@ export interface WorkflowStep {
2324
interactive?: boolean; // Controls waiting behavior: true=wait for input, false=auto-advance
2425
tracks?: string[]; // Track names this step belongs to (e.g., ['bmad', 'enterprise'])
2526
conditions?: string[]; // Conditions required for this step (e.g., ['has_ui', 'has_api'])
27+
conditionsAny?: string[]; // OR conditions for this step (e.g., ['full-workflow', 'workflow-definition'])
2628
}
2729

2830
export interface LoopBehaviorConfig {

0 commit comments

Comments
 (0)