Skip to content

Commit e20cd7f

Browse files
committed
feat(workflows): add conditional chained prompts support
- Introduce `selectedConditions` parameter to filter chained prompt paths - Support conditional path entries in agent configuration - Refactor chained prompt loading to handle conditional paths
1 parent bc86f27 commit e20cd7f

File tree

7 files changed

+100
-17
lines changed

7 files changed

+100
-17
lines changed

config/main.agents.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ module.exports = [
8181
path.join(promptsDir, 'bmad', '01-analyst', 'workflow.md'),
8282
path.join(promptsDir, 'bmad', '01-analyst', 'chained', 'step-01-vision.md'),
8383
],
84-
chainedPromptsPath: path.join(promptsDir, 'bmad', '01-analyst', 'chained'),
84+
chainedPromptsPath: [
85+
path.join(promptsDir, 'bmad', '01-analyst', 'chained'),
86+
],
8587
},
8688

8789
// Test agents

src/agents/runner/chained.ts

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs';
22
import * as path from 'node:path';
33

44
import { debug } from '../../shared/logging/logger.js';
5+
import type { ChainedPathEntry, ConditionalChainedPath } from '../../shared/agents/config/types.js';
56

67
/**
78
* Represents a chained prompt loaded from a .md file
@@ -87,21 +88,16 @@ function filenameToName(filename: string): string {
8788
}
8889

8990
/**
90-
* Load chained prompts from a folder
91-
* Files are sorted by filename (01-first.md, 02-second.md, etc.)
92-
*
93-
* @param chainedPromptsPath - Absolute or relative path to folder containing .md files
94-
* @param projectRoot - Project root for resolving relative paths
95-
* @returns Array of ChainedPrompt objects sorted by filename
91+
* Load chained prompts from a single folder
9692
*/
97-
export async function loadChainedPrompts(
98-
chainedPromptsPath: string,
93+
async function loadPromptsFromFolder(
94+
folderPath: string,
9995
projectRoot: string
10096
): Promise<ChainedPrompt[]> {
10197
// Resolve path
102-
const absolutePath = path.isAbsolute(chainedPromptsPath)
103-
? chainedPromptsPath
104-
: path.resolve(projectRoot, chainedPromptsPath);
98+
const absolutePath = path.isAbsolute(folderPath)
99+
? folderPath
100+
: path.resolve(projectRoot, folderPath);
105101

106102
// Check if directory exists
107103
try {
@@ -147,3 +143,60 @@ export async function loadChainedPrompts(
147143
debug(`Loaded ${prompts.length} chained prompts from ${absolutePath}`);
148144
return prompts;
149145
}
146+
147+
/**
148+
* Type guard for conditional path entry
149+
*/
150+
function isConditionalPath(entry: ChainedPathEntry): entry is ConditionalChainedPath {
151+
return typeof entry === 'object' && entry !== null && 'path' in entry;
152+
}
153+
154+
/**
155+
* Check if all conditions are met (AND logic)
156+
*/
157+
function meetsConditions(entry: ChainedPathEntry, selectedConditions: string[]): boolean {
158+
if (typeof entry === 'string') return true;
159+
if (!entry.conditions?.length) return true;
160+
return entry.conditions.every(c => selectedConditions.includes(c));
161+
}
162+
163+
/**
164+
* Extract path string from entry
165+
*/
166+
function getPath(entry: ChainedPathEntry): string {
167+
return typeof entry === 'string' ? entry : entry.path;
168+
}
169+
170+
/**
171+
* Load chained prompts from one or more folders
172+
* Files are sorted by filename within each folder (01-first.md, 02-second.md, etc.)
173+
* When multiple folders are provided, prompts are loaded in folder order
174+
*
175+
* @param chainedPromptsPath - Path or array of paths to folder(s) containing .md files
176+
* @param projectRoot - Project root for resolving relative paths
177+
* @param selectedConditions - User-selected conditions for filtering conditional paths
178+
* @returns Array of ChainedPrompt objects sorted by filename within each folder
179+
*/
180+
export async function loadChainedPrompts(
181+
chainedPromptsPath: ChainedPathEntry | ChainedPathEntry[],
182+
projectRoot: string,
183+
selectedConditions: string[] = []
184+
): Promise<ChainedPrompt[]> {
185+
const entries = Array.isArray(chainedPromptsPath) ? chainedPromptsPath : [chainedPromptsPath];
186+
const allPrompts: ChainedPrompt[] = [];
187+
188+
for (const entry of entries) {
189+
if (!meetsConditions(entry, selectedConditions)) {
190+
const pathStr = getPath(entry);
191+
const conditions = isConditionalPath(entry) ? entry.conditions : [];
192+
debug(`Skipped chained path: ${pathStr} (unmet conditions: ${conditions?.join(', ')})`);
193+
continue;
194+
}
195+
196+
const folderPath = getPath(entry);
197+
const prompts = await loadPromptsFromFolder(folderPath, projectRoot);
198+
allPrompts.push(...prompts);
199+
}
200+
201+
return allPrompts;
202+
}

src/agents/runner/runner.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ export interface ExecuteAgentOptions {
135135
* Custom prompt for resume (instead of "Continue from where you left off")
136136
*/
137137
resumePrompt?: string;
138+
139+
/**
140+
* Selected conditions for filtering conditional chained prompt paths
141+
*/
142+
selectedConditions?: string[];
138143
}
139144

140145
/**
@@ -195,7 +200,7 @@ export async function executeAgent(
195200
prompt: string,
196201
options: ExecuteAgentOptions,
197202
): Promise<AgentExecutionOutput> {
198-
const { workingDir, projectRoot, engine: engineOverride, model: modelOverride, logger, stderrLogger, onTelemetry, abortSignal, timeout, parentId, disableMonitoring, ui, uniqueAgentId, displayPrompt, resumeMonitoringId, resumePrompt } = options;
203+
const { workingDir, projectRoot, engine: engineOverride, model: modelOverride, logger, stderrLogger, onTelemetry, abortSignal, timeout, parentId, disableMonitoring, ui, uniqueAgentId, displayPrompt, resumeMonitoringId, resumePrompt, selectedConditions } = options;
199204

200205
// If resuming, look up session info from monitor
201206
let resumeSessionId: string | undefined;
@@ -416,7 +421,8 @@ export async function executeAgent(
416421
if (agentConfig.chainedPromptsPath) {
417422
chainedPrompts = await loadChainedPrompts(
418423
agentConfig.chainedPromptsPath,
419-
projectRoot ?? workingDir
424+
projectRoot ?? workingDir,
425+
selectedConditions ?? []
420426
);
421427
if (chainedPrompts.length > 0) {
422428
debug(`Loaded ${chainedPrompts.length} chained prompts for agent '${agentId}'`);

src/shared/agents/config/types.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1+
/**
2+
* Conditional chained prompt path entry
3+
*/
4+
export type ConditionalChainedPath = {
5+
path: string;
6+
conditions?: string[];
7+
};
8+
9+
/**
10+
* Single chained prompt path entry - string or conditional object
11+
*/
12+
export type ChainedPathEntry = string | ConditionalChainedPath;
13+
114
export type AgentDefinition = {
215
id: string;
316
model?: unknown;
417
modelReasoningEffort?: unknown;
518
model_reasoning_effort?: unknown;
619
engine?: string; // Engine to use for this agent (dynamically determined from registry)
7-
chainedPromptsPath?: string; // Path to folder containing chained prompt .md files
20+
chainedPromptsPath?: ChainedPathEntry | ChainedPathEntry[]; // Path(s) to folder(s) containing chained prompt .md files
821
[key: string]: unknown;
922
};
1023

src/workflows/execution/resume.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ interface ExecWithResumeOptions {
4141
shouldResumeFromPause: boolean;
4242
stepResumeMonitoringId: number | undefined;
4343
stepResumePrompt: string | undefined;
44+
selectedConditions: string[];
4445
}
4546

4647
interface ExecWithResumeResult {
@@ -68,6 +69,7 @@ export async function execWithResume(options: ExecWithResumeOptions): Promise<Ex
6869
shouldResumeFromPause,
6970
stepResumeMonitoringId,
7071
stepResumePrompt,
72+
selectedConditions,
7173
} = options;
7274

7375
debug(`[DEBUG workflow] Checking if fallback should execute... notCompletedSteps=${JSON.stringify(notCompletedSteps)}`);
@@ -114,7 +116,8 @@ export async function execWithResume(options: ExecWithResumeOptions): Promise<Ex
114116
if (agentConfig?.chainedPromptsPath) {
115117
stepOutput.chainedPrompts = await loadChainedPrompts(
116118
agentConfig.chainedPromptsPath,
117-
cwd
119+
cwd,
120+
selectedConditions
118121
);
119122
}
120123
} else if (shouldResumeFromSavedSession && stepDataForResume?.monitoringId) {
@@ -139,7 +142,7 @@ export async function execWithResume(options: ExecWithResumeOptions): Promise<Ex
139142
stepOutput = {
140143
output: '',
141144
monitoringId: stepDataForResume.monitoringId,
142-
chainedPrompts: await loadChainedPrompts(agentConfig.chainedPromptsPath!, cwd),
145+
chainedPrompts: await loadChainedPrompts(agentConfig.chainedPromptsPath!, cwd, selectedConditions),
143146
};
144147
} else {
145148
// No chained prompts - normal resume with re-execution
@@ -154,6 +157,7 @@ export async function execWithResume(options: ExecWithResumeOptions): Promise<Ex
154157
uniqueAgentId,
155158
resumeMonitoringId: stepResumeMonitoringId,
156159
resumePrompt: stepResumePrompt,
160+
selectedConditions,
157161
});
158162

159163
debug(`[DEBUG workflow] executeStep completed. monitoringId=${stepOutput.monitoringId}`);
@@ -184,6 +188,7 @@ export async function execWithResume(options: ExecWithResumeOptions): Promise<Ex
184188
uniqueAgentId,
185189
resumeMonitoringId: stepResumeMonitoringId,
186190
resumePrompt: stepResumePrompt,
191+
selectedConditions,
187192
});
188193

189194
debug(`[DEBUG workflow] executeStep completed. monitoringId=${stepOutput.monitoringId}`);

src/workflows/execution/run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ export async function runWorkflow(options: RunWorkflowOptions = {}): Promise<voi
385385
shouldResumeFromPause,
386386
stepResumeMonitoringId,
387387
stepResumePrompt,
388+
selectedConditions,
388389
});
389390

390391
debug(`[DEBUG workflow] Checking for chained prompts...`);

src/workflows/execution/step.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export interface StepExecutorOptions {
3535
resumeMonitoringId?: number;
3636
/** Custom prompt for resume (instead of "Continue from where you left off") */
3737
resumePrompt?: string;
38+
/** Selected conditions for filtering conditional chained prompt paths */
39+
selectedConditions?: string[];
3840
}
3941

4042
async function ensureProjectScaffold(cwd: string): Promise<void> {
@@ -133,6 +135,7 @@ export async function executeStep(
133135
uniqueAgentId: options.uniqueAgentId,
134136
resumeMonitoringId: options.resumeMonitoringId,
135137
resumePrompt: options.resumePrompt,
138+
selectedConditions: options.selectedConditions,
136139
// Pass emitter as UI so runner can register monitoring ID immediately
137140
ui: options.emitter,
138141
});

0 commit comments

Comments
 (0)