Skip to content

Commit 97bba7b

Browse files
committed
refactor(controller): centralize engine/model tracking in MonitorService
Remove engine/model from ControllerConfig persistence and consistently use MonitorService as single source of truth Add error handling for controller view execution Update all controller-related components to fetch engine/model from MonitorService
1 parent 4613eaa commit 97bba7b

8 files changed

Lines changed: 59 additions & 33 deletions

File tree

‎src/cli/tui/routes/workflow/hooks/use-workflow-events.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { onMount, onCleanup, createSignal, type Accessor } from "solid-js"
88
import { OpenTUIAdapter } from "../adapters/opentui"
99
import { loadControllerConfig } from "../../../../../workflows/controller/config.js"
10+
import { AgentMonitorService } from "../../../../../agents/monitoring/index.js"
1011
import { debug } from "../../../../../shared/logging/logger.js"
1112
import path from "path"
1213
import type { WorkflowEventBus } from "../../../../../workflows/events/index.js"
@@ -79,12 +80,17 @@ export function useWorkflowEvents(options: UseWorkflowEventsOptions): UseWorkflo
7980
// This enables 'C' key to return to controller even after restart/resume
8081
if (controllerState?.controllerConfig?.agentId) {
8182
debug('onMount - setting controllerState in UI from persisted config')
83+
// Get engine/model from MonitorService (single source of truth)
84+
const monitor = AgentMonitorService.getInstance()
85+
const monitoringId = Number(controllerState.controllerConfig.monitoringId) || undefined
86+
const controllerAgent = monitoringId ? monitor.getAgent(monitoringId) : undefined
8287
actions.setControllerState({
8388
id: controllerState.controllerConfig.agentId,
8489
name: controllerState.controllerConfig.agentId, // Name may not be persisted, use agentId as fallback
85-
engine: 'unknown', // Engine info not persisted in controller config
90+
engine: controllerAgent?.engine ?? 'unknown',
91+
model: controllerAgent?.modelName,
8692
telemetry: { tokensIn: 0, tokensOut: 0 }, // Default telemetry, will be updated if controller runs
87-
monitoringId: Number(controllerState.controllerConfig.monitoringId) || undefined,
93+
monitoringId,
8894
})
8995
}
9096

‎src/shared/workflows/template.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,12 @@ export interface StepData {
2626

2727
/**
2828
* Controller configuration for autonomous mode
29+
* Note: engine/model are NOT persisted here - read from MonitorService instead
2930
*/
3031
export interface ControllerConfig {
3132
agentId: string;
3233
sessionId: string;
3334
monitoringId: number;
34-
/** Engine used for this controller session (for resume) */
35-
engine?: string;
36-
/** Model used for this controller session (for resume) */
37-
model?: string;
3835
}
3936

4037
interface TemplateTracking {

‎src/workflows/controller/init.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,11 @@ export async function initControllerAgent(
9292
const resolvedModel = agent.modelName;
9393
debug('[Controller] Resolved engine=%s model=%s from MonitorService', resolvedEngine, resolvedModel);
9494

95-
// Build config (include engine/model for resume)
95+
// Build config for persistence (engine/model NOT saved - read from MonitorService)
9696
const config: ControllerConfig = {
9797
agentId,
9898
sessionId: agent.sessionId,
9999
monitoringId: result.agentId!,
100-
engine: resolvedEngine,
101-
model: resolvedModel,
102100
};
103101
debug('[Controller] Built config: %o', config);
104102

@@ -107,6 +105,7 @@ export async function initControllerAgent(
107105
await saveControllerConfig(cmRoot, config);
108106
debug('[Controller] Controller config saved successfully');
109107

108+
// Return includes engine/model from MonitorService for UI display
110109
return {
111110
...config,
112111
engine: resolvedEngine,

‎src/workflows/controller/view.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,14 @@ export async function runControllerView(
9090

9191
// Return controller info for deferred emission (after workflow:started)
9292
// This enables the 'c' key to return to controller even while step agent executes
93+
// Get engine/model from MonitorService (single source of truth)
94+
const monitor = AgentMonitorService.getInstance();
95+
const controllerAgent = monitor.getAgent(existingConfig.controllerConfig.monitoringId);
9396
const controllerInfo = controller ? {
9497
id: definition.agentId,
9598
name: (controller.name as string | undefined) ?? controller.id,
96-
engine: existingConfig.controllerConfig.engine || 'unknown',
97-
model: existingConfig.controllerConfig.model,
99+
engine: controllerAgent?.engine ?? 'unknown',
100+
model: controllerAgent?.modelName,
98101
} : undefined;
99102

100103
// Transition to executing view with autonomous mode

‎src/workflows/indexing/types.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,10 @@ export interface TemplateTracking {
8888

8989
/**
9090
* Controller configuration for autonomous mode
91+
* Note: engine/model are NOT persisted here - read from MonitorService instead
9192
*/
9293
export interface ControllerConfig {
9394
agentId: string;
9495
sessionId: string;
9596
monitoringId: number;
96-
/** Engine used for this controller session (for resume) */
97-
engine?: string;
98-
/** Model used for this controller session (for resume) */
99-
model?: string;
10097
}

‎src/workflows/input/providers/controller.ts‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
import { controllerTemplateReview } from '../../../shared/prompts/index.js';
1919
import type { ControllerConfig } from '../../../shared/workflows/template.js';
2020
import type { WorkflowEventEmitter } from '../../events/index.js';
21-
import { registry } from '../../../infra/engines/index.js';
2221
import type {
2322
InputProvider,
2423
InputContext,
@@ -123,11 +122,11 @@ export class ControllerInputProvider implements InputProvider {
123122
const agentConfig = await loadAgentConfig(config.agentId, this.cmRoot);
124123
const controllerName = agentConfig.name || config.agentId;
125124

126-
// Resolve engine and model from config or defaults
127-
const defaultEngine = registry.getDefault();
128-
const resolvedEngine = (agentConfig.engine as string) ?? defaultEngine?.metadata.id ?? 'unknown';
129-
const engineModule = registry.get(resolvedEngine);
130-
const resolvedModel = (agentConfig.model as string | undefined) ?? engineModule?.metadata.defaultModel;
125+
// Get engine/model from MonitorService (single source of truth)
126+
const monitor = AgentMonitorService.getInstance();
127+
const controllerAgent = monitor.getAgent(config.monitoringId);
128+
const resolvedEngine = controllerAgent?.engine ?? 'unknown';
129+
const resolvedModel = controllerAgent?.modelName ?? 'unknown';
131130

132131
// Emit controller info to UI
133132
if (this.workflowEmitter) {
@@ -146,8 +145,8 @@ export class ControllerInputProvider implements InputProvider {
146145
// Resume controller session - reuse existing monitoring entry to avoid duplicate agent/log registration
147146
const result = await executeWithActions(config.agentId, prompt, {
148147
workingDir: this.cwd,
149-
engine: config.engine,
150-
model: config.model,
148+
engine: resolvedEngine !== 'unknown' ? resolvedEngine : undefined,
149+
model: resolvedModel !== 'unknown' ? resolvedModel : undefined,
151150
resumeSessionId: config.sessionId,
152151
resumeMonitoringId: config.monitoringId,
153152
resumePrompt: prompt,

‎src/workflows/run.ts‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,23 @@ export async function runWorkflow(options: RunWorkflowOptions = {}): Promise<voi
219219

220220
// Run controller view FIRST if needed (blocks until controller done + user confirms)
221221
// Timeline population happens AFTER controller view since it's only visible in executing view
222-
const controllerResult = await runControllerView({
223-
cwd,
224-
cmRoot,
225-
template,
226-
emitter,
227-
eventBus,
228-
});
222+
let controllerResult;
223+
try {
224+
controllerResult = await runControllerView({
225+
cwd,
226+
cmRoot,
227+
template,
228+
emitter,
229+
eventBus,
230+
});
231+
} catch (error) {
232+
debug('[Workflow] Controller view error: %s', (error as Error).message);
233+
emitter.setWorkflowStatus('error');
234+
(process as NodeJS.EventEmitter).emit('workflow:error', {
235+
reason: (error as Error).message,
236+
});
237+
throw error;
238+
}
229239

230240
// If controller ran, adjust start index to skip the controller agent step
231241
let actualStartIndex = startIndex;

‎src/workflows/signals/handlers/return.ts‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import { debug } from '../../../shared/logging/logger.js'
99
import { loadControllerConfig, setAutonomousMode } from '../../controller/config.js'
1010
import { setControllerView } from '../../../shared/workflows/template.js'
1111
import { formatUserInput } from '../../../shared/formatters/outputMarkers.js'
12-
import { AgentLoggerService } from '../../../agents/monitoring/index.js'
12+
import { AgentLoggerService, AgentMonitorService } from '../../../agents/monitoring/index.js'
1313
import { executeAgent } from '../../../agents/runner/runner.js'
14+
import { loadAgentConfig } from '../../../agents/runner/config.js'
1415
import type { SignalContext } from '../manager/types.js'
1516

1617
/**
@@ -76,6 +77,18 @@ export async function handleReturnToControllerSignal(ctx: SignalContext): Promis
7677
ctx.emitter.updateControllerStatus('awaiting')
7778
debug('[ReturnToControllerSignal] After updateControllerStatus')
7879

80+
// Emit controller info with engine from MonitorService (single source of truth)
81+
const monitor = AgentMonitorService.getInstance()
82+
const controllerAgent = monitor.getAgent(config.monitoringId)
83+
const agentConfig = await loadAgentConfig(config.agentId, ctx.cmRoot)
84+
const controllerName = agentConfig.name || config.agentId
85+
ctx.emitter.setControllerInfo(
86+
config.agentId,
87+
controllerName,
88+
controllerAgent?.engine ?? 'unknown',
89+
controllerAgent?.modelName ?? 'unknown'
90+
)
91+
7992
// Set autonomous mode to never for conversation (file persistence)
8093
await setAutonomousMode(ctx.cmRoot, 'never')
8194

@@ -132,13 +145,15 @@ export async function handleReturnToControllerSignal(ctx: SignalContext): Promis
132145
const formatted = formatUserInput(data.prompt)
133146
AgentLoggerService.getInstance().write(config.monitoringId, `\n${formatted}\n`)
134147

148+
// Get engine/model from MonitorService for resume
149+
const agentInfo = monitor.getAgent(config.monitoringId)
135150
await executeAgent(config.agentId, data.prompt, {
136151
workingDir: ctx.cwd,
137152
resumeSessionId: config.sessionId,
138153
resumePrompt: data.prompt,
139154
resumeMonitoringId: config.monitoringId,
140-
engine: config.engine,
141-
model: config.model,
155+
engine: agentInfo?.engine,
156+
model: agentInfo?.modelName,
142157
})
143158

144159
// After response, go back to awaiting input

0 commit comments

Comments
 (0)