Skip to content

Commit 1b9e05c

Browse files
committed
feat(controller): improve controller input handling and session management
- Add cmRoot to controller options for config persistence - Enhance prompt building with cleaned step output and reminders - Update session ID handling to maintain persistence - Improve logging in workflow emitter and agent runner - Add debug logging to log stream hook - Refactor controller prompt extraction and color marker removal - Update PO controller prompt with detailed instructions
1 parent dcad61e commit 1b9e05c

File tree

14 files changed

+230
-42
lines changed

14 files changed

+230
-42
lines changed

config/main.agents.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ module.exports = [
7777
name: 'PO - Product Owner',
7878
description: 'BMAD product owner controller for autonomous mode',
7979
role: 'controller',
80-
promptPath: path.join(promptsDir, 'bmad', 'controller', 'system.md'),
80+
promptPath: path.join(promptsDir, 'bmad', 'controller', 'PO.md'),
8181
},
8282

8383
// BMAD agents
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Product Owner Controller
2+
3+
You are a Product Owner for {project_name}. You represent the user's business interests and collaborate with the working agent.
4+
5+
## PROJECT SPECIFICATIONS
6+
7+
```
8+
{specifications}
9+
```
10+
11+
## FIRST MESSAGE
12+
13+
Say only: "I am ready!"
14+
15+
Then wait for the agent.
16+
17+
## HOW TO RESPOND
18+
19+
### When Agent Asks Questions
20+
21+
Answer from business perspective using your specifications knowledge:
22+
23+
- Give clear, decisive answers
24+
- Share business context and requirements
25+
- Help the agent understand what users need
26+
- Don't ask questions back - make decisions
27+
28+
Example:
29+
Agent: "Who are the target users?"
30+
You: "Individual professionals and students who need simple personal task management. No team features needed for MVP."
31+
32+
### When Agent Presents Work
33+
34+
If work meets business needs:
35+
- Say why it's acceptable (business perspective)
36+
- Say `ACTION: NEXT`
37+
38+
Example:
39+
"This covers the core CRUD functionality our users need. Simple and focused on MVP scope.
40+
41+
ACTION: NEXT"
42+
43+
If work needs changes:
44+
- Say what's wrong (business perspective)
45+
- Say what you expect instead
46+
47+
Example:
48+
"Too complex for MVP. Users just need basic add/edit/delete/complete. Remove the project management features."
49+
50+
### When Step is Complete
51+
52+
After agent confirms step completion and saves artifacts:
53+
- Acknowledge the deliverable
54+
- Say `ACTION: NEXT` to proceed
55+
56+
## ACTION COMMANDS
57+
58+
| Command | When to Use |
59+
|---------|-------------|
60+
| `ACTION: NEXT` | Step complete, proceed to next |
61+
| `ACTION: STOP` | Fatal error, cannot continue |
62+
63+
## RULES
64+
65+
1. Text responses only - never use tools
66+
2. Be decisive - don't ask questions, make decisions
67+
3. Answer from BUSINESS perspective, not technical
68+
4. First message is always "I am ready!"

prompts/templates/bmad/controller/system.md

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
You are Test Agent 1. You will say your name.
1+
You are Test Agent 1. Ask the user: "What is the user asked you for?"

src/agents/runner/runner.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,18 +207,26 @@ export async function executeAgent(
207207
): Promise<AgentExecutionOutput> {
208208
const { workingDir, projectRoot, engine: engineOverride, model: modelOverride, logger, stderrLogger, onTelemetry, abortSignal, timeout, parentId, disableMonitoring, ui, uniqueAgentId, displayPrompt, resumeMonitoringId, resumePrompt, resumeSessionId: resumeSessionIdOption, selectedConditions } = options;
209209

210+
debug(`[AgentRunner] executeAgent called: agentId=%s promptLength=%d`, agentId, prompt.length);
211+
debug(`[AgentRunner] Options: workingDir=%s engineOverride=%s modelOverride=%s parentId=%s`,
212+
workingDir, engineOverride ?? '(none)', modelOverride ?? '(none)', parentId ?? '(none)');
213+
debug(`[AgentRunner] Resume options: resumeMonitoringId=%s resumeSessionId=%s resumePrompt=%s`,
214+
resumeMonitoringId ?? '(none)', resumeSessionIdOption ?? '(none)', resumePrompt ? resumePrompt.slice(0, 50) + '...' : '(none)');
215+
210216
// If resuming, use direct sessionId or look up from monitor
211217
let resumeSessionId: string | undefined = resumeSessionIdOption;
212218
if (!resumeSessionId && resumeMonitoringId !== undefined) {
213219
const monitor = AgentMonitorService.getInstance();
214220
const resumeAgent = monitor.getAgent(resumeMonitoringId);
221+
debug(`[AgentRunner] Looking up sessionId from monitoringId=%d, found agent=%s`,
222+
resumeMonitoringId, resumeAgent ? 'yes' : 'no');
215223
if (resumeAgent?.sessionId) {
216224
resumeSessionId = resumeAgent.sessionId;
217-
debug(`[RESUME] Using sessionId ${resumeSessionId} from monitoringId ${resumeMonitoringId}`);
225+
debug(`[AgentRunner] Using sessionId %s from monitoringId %d`, resumeSessionId, resumeMonitoringId);
218226
}
219227
}
220228
if (resumeSessionId) {
221-
debug(`[RESUME] Resuming with sessionId: ${resumeSessionId}`);
229+
debug(`[AgentRunner] Will resume with sessionId: %s`, resumeSessionId);
222230
}
223231

224232
// Load agent config to determine engine and model
@@ -284,18 +292,21 @@ export async function executeAgent(
284292
if (resumeMonitoringId !== undefined) {
285293
// RESUME: Use existing monitoring entry (skip registration, use existing log file)
286294
monitoringAgentId = resumeMonitoringId;
287-
debug(`[RESUME] Using existing monitoringId ${monitoringAgentId}, skipping registration`);
295+
debug(`[AgentRunner] RESUME: Using existing monitoringId=%d, skipping registration`, monitoringAgentId);
288296

289297
// Mark as running again (was paused)
290298
await monitor.markRunning(monitoringAgentId);
299+
debug(`[AgentRunner] RESUME: Marked agent as running`);
291300

292301
// Register monitoring ID with UI so it can load existing logs
293302
if (ui && uniqueAgentId) {
303+
debug(`[AgentRunner] RESUME: Registering monitoringId=%d with UI (uniqueAgentId=%s)`, monitoringAgentId, uniqueAgentId);
294304
ui.registerMonitoringId(uniqueAgentId, monitoringAgentId);
295305
}
296306
} else {
297307
// NEW EXECUTION: Register new monitoring entry
298308
const promptForDisplay = displayPrompt || prompt;
309+
debug(`[AgentRunner] NEW: Registering new monitoring entry for agentId=%s`, agentId);
299310
monitoringAgentId = await monitor.register({
300311
name: agentId,
301312
prompt: promptForDisplay, // This gets truncated in monitor for memory efficiency
@@ -304,13 +315,15 @@ export async function executeAgent(
304315
engineProvider: engineType,
305316
modelName: model,
306317
});
318+
debug(`[AgentRunner] NEW: Registered with monitoringId=%d`, monitoringAgentId);
307319

308320
// Store FULL prompt for debug mode logging (not the display prompt)
309321
// In debug mode, we want to see the complete composite prompt with template + input files
310322
loggerService.storeFullPrompt(monitoringAgentId, prompt);
311323

312324
// Register monitoring ID with UI immediately so it can load logs
313325
if (ui && uniqueAgentId && monitoringAgentId !== undefined) {
326+
debug(`[AgentRunner] NEW: Registering monitoringId=%d with UI (uniqueAgentId=%s)`, monitoringAgentId, uniqueAgentId);
314327
ui.registerMonitoringId(uniqueAgentId, monitoringAgentId);
315328
}
316329
}
@@ -324,6 +337,8 @@ export async function executeAgent(
324337
// Get engine and execute
325338
// NOTE: Prompt is already complete - no template loading or building here
326339
const engine = getEngine(engineType);
340+
debug(`[AgentRunner] Starting engine execution: engine=%s model=%s resumeSessionId=%s`,
341+
engineType, model, resumeSessionId ?? '(new session)');
327342

328343
let totalStdout = '';
329344

@@ -416,8 +431,11 @@ export async function executeAgent(
416431
timestamp: new Date().toISOString(),
417432
});
418433

434+
debug(`[AgentRunner] Engine execution completed, outputLength=%d`, totalStdout.length);
435+
419436
// Mark agent as completed
420437
if (monitor && monitoringAgentId !== undefined) {
438+
debug(`[AgentRunner] Marking agent %d as completed`, monitoringAgentId);
421439
await monitor.complete(monitoringAgentId);
422440
// Note: Don't close stream here - workflow may write more messages
423441
// Streams will be closed by cleanup handlers or monitoring service shutdown
@@ -427,7 +445,7 @@ export async function executeAgent(
427445
// Always load on fresh execution; on resume, workflow.ts decides whether to use them
428446
// based on chain resume state (chainResumeInfo)
429447
let chainedPrompts: ChainedPrompt[] | undefined;
430-
debug(`[ChainedPrompts] agentConfig.chainedPromptsPath: ${agentConfig.chainedPromptsPath}`);
448+
debug(`[AgentRunner] ChainedPrompts path: %s`, agentConfig.chainedPromptsPath ?? '(none)');
431449
if (agentConfig.chainedPromptsPath) {
432450
chainedPrompts = await loadChainedPrompts(
433451
agentConfig.chainedPromptsPath,
@@ -439,21 +457,30 @@ export async function executeAgent(
439457
debug(`[ChainedPrompts] No chainedPromptsPath for agent '${agentId}'`);
440458
}
441459

460+
debug(`[AgentRunner] Returning result: monitoringAgentId=%d chainedPrompts=%d`,
461+
monitoringAgentId ?? -1, chainedPrompts?.length ?? 0);
462+
442463
return {
443464
output: stdout,
444465
agentId: monitoringAgentId,
445466
chainedPrompts,
446467
};
447-
} catch (error) {
468+
} catch (err) {
469+
debug(`[AgentRunner] Error during execution: %s`, (err as Error).message);
470+
448471
// Mark agent as failed (unless already paused - that means intentional abort)
449472
if (monitor && monitoringAgentId !== undefined) {
450473
const agent = monitor.getAgent(monitoringAgentId);
474+
debug(`[AgentRunner] Agent status: %s`, agent?.status ?? '(not found)');
451475
if (agent?.status !== 'paused') {
452-
await monitor.fail(monitoringAgentId, error as Error);
476+
debug(`[AgentRunner] Marking agent %d as failed`, monitoringAgentId);
477+
await monitor.fail(monitoringAgentId, err as Error);
478+
} else {
479+
debug(`[AgentRunner] Agent is paused, not marking as failed`);
453480
}
454481
// Note: Don't close stream here - workflow may write more messages
455482
// Streams will be closed by cleanup handlers or monitoring service shutdown
456483
}
457-
throw error;
484+
throw err;
458485
}
459486
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,19 @@ export function OutputWindow(props: OutputWindowProps) {
112112

113113
// Running but not waiting for input
114114
if (status === "running" || status === "stopping") {
115+
// Preserve chained step info even when agent is working
116+
if (inputState?.queuedPrompts && inputState.queuedPrompts.length > 0) {
117+
const idx = inputState.currentIndex ?? 0
118+
const prompt = inputState.queuedPrompts[idx]
119+
return {
120+
mode: "passive",
121+
chainedStep: {
122+
name: prompt?.name ?? "next step",
123+
index: idx + 1,
124+
total: inputState.queuedPrompts.length,
125+
},
126+
}
127+
}
115128
return { mode: "passive" }
116129
}
117130

src/cli/tui/routes/workflow/components/output/prompt-line.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type PendingPaste = { placeholder: string; content: string }
2222

2323
export type PromptLineState =
2424
| { mode: "disabled" }
25-
| { mode: "passive" }
25+
| { mode: "passive"; chainedStep?: { name: string; index: number; total: number } }
2626
| { mode: "active"; reason?: "paused" | "chaining" }
2727
| { mode: "chained"; name: string; description: string; index: number; total: number }
2828

@@ -306,6 +306,11 @@ export function PromptLine(props: PromptLineProps) {
306306
}
307307

308308
const getHint = () => {
309+
// Show chained step info even in passive mode
310+
if (props.state.mode === "passive" && props.state.chainedStep) {
311+
const step = props.state.chainedStep
312+
return `Step ${step.index}/${step.total}: ${step.name}`
313+
}
309314
if (!isInteractive()) return null
310315
if (props.state.mode === "chained") {
311316
return `Step ${props.state.index}/${props.state.total}: ${props.state.name}`

0 commit comments

Comments
 (0)