Skip to content

Commit 85608a7

Browse files
committed
feat(workflow): add autonomous mode with controller integration
implement autonomous workflow execution controlled by an AI agent add UI indicators and keyboard shortcut for autonomous mode persist autonomous mode state across workflow sessions enhance controller loop with action handling and session management
1 parent d566bca commit 85608a7

File tree

12 files changed

+156
-56
lines changed

12 files changed

+156
-56
lines changed

src/agents/runner/runner.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ export interface ExecuteAgentOptions {
136136
*/
137137
resumePrompt?: string;
138138

139+
/**
140+
* Session ID for resuming (direct, for when monitoringId is not available)
141+
*/
142+
resumeSessionId?: string;
143+
139144
/**
140145
* Selected conditions for filtering conditional chained prompt paths
141146
*/
@@ -200,18 +205,21 @@ export async function executeAgent(
200205
prompt: string,
201206
options: ExecuteAgentOptions,
202207
): Promise<AgentExecutionOutput> {
203-
const { workingDir, projectRoot, engine: engineOverride, model: modelOverride, logger, stderrLogger, onTelemetry, abortSignal, timeout, parentId, disableMonitoring, ui, uniqueAgentId, displayPrompt, resumeMonitoringId, resumePrompt, selectedConditions } = options;
208+
const { workingDir, projectRoot, engine: engineOverride, model: modelOverride, logger, stderrLogger, onTelemetry, abortSignal, timeout, parentId, disableMonitoring, ui, uniqueAgentId, displayPrompt, resumeMonitoringId, resumePrompt, resumeSessionId: resumeSessionIdOption, selectedConditions } = options;
204209

205-
// If resuming, look up session info from monitor
206-
let resumeSessionId: string | undefined;
207-
if (resumeMonitoringId !== undefined) {
210+
// If resuming, use direct sessionId or look up from monitor
211+
let resumeSessionId: string | undefined = resumeSessionIdOption;
212+
if (!resumeSessionId && resumeMonitoringId !== undefined) {
208213
const monitor = AgentMonitorService.getInstance();
209214
const resumeAgent = monitor.getAgent(resumeMonitoringId);
210215
if (resumeAgent?.sessionId) {
211216
resumeSessionId = resumeAgent.sessionId;
212217
debug(`[RESUME] Using sessionId ${resumeSessionId} from monitoringId ${resumeMonitoringId}`);
213218
}
214219
}
220+
if (resumeSessionId) {
221+
debug(`[RESUME] Resuming with sessionId: ${resumeSessionId}`);
222+
}
215223

216224
// Load agent config to determine engine and model
217225
const agentConfig = await loadAgentConfig(agentId, projectRoot ?? workingDir);

src/cli/tui/app-shell.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { MonitoringCleanup } from "../../agents/monitoring/index.js"
2323
import path from "path"
2424
import { createRequire } from "node:module"
2525
import { resolvePackageJson } from "../../shared/runtime/root.js"
26-
import { getSelectedTrack, setSelectedTrack, hasSelectedConditions, setSelectedConditions, getProjectName, setProjectName, getControllerAgents, initControllerAgent } from "../../shared/workflows/index.js"
26+
import { getSelectedTrack, setSelectedTrack, hasSelectedConditions, setSelectedConditions, getProjectName, setProjectName, getControllerAgents, initControllerAgent, loadControllerConfig } from "../../shared/workflows/index.js"
2727
import { loadTemplate } from "../../workflows/templates/loader.js"
2828
import { getTemplatePathFromTracking } from "../../shared/workflows/template.js"
2929
import type { TrackConfig, ConditionConfig } from "../../workflows/templates/types"
@@ -158,8 +158,11 @@ export function App(props: { initialToast?: InitialToast }) {
158158
const needsProjectName = !existingProjectName
159159

160160
// Check if workflow requires controller selection
161+
// Skip if controller session already exists
161162
let controllers: AgentDefinition[] = []
162-
if (template.controller === true) {
163+
const existingControllerConfig = await loadControllerConfig(cmRoot)
164+
const hasExistingControllerSession = existingControllerConfig?.controllerConfig?.sessionId
165+
if (template.controller === true && !hasExistingControllerSession) {
163166
controllers = await getControllerAgents(cwd)
164167
}
165168
const needsControllerSelection = controllers.length > 0

src/cli/tui/routes/workflow/components/output/status-footer.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,27 @@
66
* Show keyboard shortcuts at bottom of screen
77
*/
88

9+
import { Show } from "solid-js"
910
import { useTheme } from "@tui/shared/context/theme"
1011

12+
export interface StatusFooterProps {
13+
autonomousMode?: boolean
14+
}
15+
1116
/**
1217
* Show keyboard shortcuts at bottom of screen
1318
*/
14-
export function StatusFooter() {
19+
export function StatusFooter(props: StatusFooterProps) {
1520
const themeCtx = useTheme()
1621

1722
return (
1823
<box paddingLeft={1} paddingRight={1}>
1924
<text fg={themeCtx.theme.textMuted}>
2025
[↑↓] Navigate [ENTER] Expand/View [Tab] Toggle Panel [H] History [P] Pause [Ctrl+S] Skip [Esc] Stop
2126
</text>
27+
<Show when={props.autonomousMode}>
28+
<text fg={themeCtx.theme.primary}> [Shift+Tab] Disable Auto</text>
29+
</Show>
2230
</box>
2331
)
2432
}

src/cli/tui/routes/workflow/components/output/telemetry-bar.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface TelemetryBarProps {
2121
tokensOut: number
2222
cached?: number
2323
}
24+
autonomousMode?: boolean
2425
}
2526

2627
/**
@@ -77,7 +78,7 @@ export function TelemetryBar(props: TelemetryBarProps) {
7778
borderStyle="rounded"
7879
borderColor={themeCtx.theme.border}
7980
>
80-
{/* Left side: workflow name, runtime, status */}
81+
{/* Left side: workflow name, runtime, status, autonomous mode */}
8182
<box flexDirection="row" flexShrink={1}>
8283
<text fg={themeCtx.theme.text} attributes={1}>
8384
{props.workflowName}
@@ -87,6 +88,10 @@ export function TelemetryBar(props: TelemetryBarProps) {
8788
<text fg={themeCtx.theme.text}></text>
8889
<text fg={statusColor()}>{statusText()}</text>
8990
</Show>
91+
<Show when={props.autonomousMode}>
92+
<text fg={themeCtx.theme.text}></text>
93+
<text fg={themeCtx.theme.primary}>AUTO</text>
94+
</Show>
9095
</box>
9196

9297
{/* Right side: token counts */}

src/cli/tui/routes/workflow/context/ui-state/actions/workflow-actions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ export function createWorkflowActions(ctx: WorkflowActionsContext) {
116116
ctx.notify()
117117
}
118118

119+
function setAutonomousMode(enabled: boolean): void {
120+
const state = ctx.getState()
121+
if (state.autonomousMode === enabled) return
122+
ctx.setState({ ...state, autonomousMode: enabled })
123+
ctx.notify()
124+
}
125+
119126
return {
120127
setWorkflowStatus,
121128
setCheckpointState,
@@ -126,5 +133,6 @@ export function createWorkflowActions(ctx: WorkflowActionsContext) {
126133
addTriggeredAgent,
127134
addUIElement,
128135
logMessage,
136+
setAutonomousMode,
129137
}
130138
}

src/cli/tui/routes/workflow/context/ui-state/initial-state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,6 @@ export function createInitialState(workflowName: string, totalSteps = 0): Workfl
3535
workflowStatus: "running",
3636
agentIdMapVersion: 0,
3737
agentLogs: new Map(),
38+
autonomousMode: false,
3839
}
3940
}

src/cli/tui/routes/workflow/context/ui-state/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export type UIActions = {
4646
resetAgentForLoop(agentId: string, cycleNumber?: number): void
4747
addUIElement(element: { id: string; text: string; stepIndex: number }): void
4848
logMessage(agentId: string, message: string): void
49+
setAutonomousMode(enabled: boolean): void
4950
}
5051

5152
export type { WorkflowState, AgentStatus, LoopState, ChainedState, InputState, SubAgentState, TriggeredAgentState, WorkflowStatus }

src/cli/tui/routes/workflow/hooks/use-workflow-keyboard.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export interface UseWorkflowKeyboardOptions {
4848
focusPromptBox?: () => void
4949
/** Exit prompt box focus */
5050
exitPromptBoxFocus?: () => void
51+
/** Check if autonomous mode is enabled */
52+
isAutonomousMode?: () => boolean
53+
/** Disable autonomous mode */
54+
disableAutonomousMode?: () => void
5155
}
5256

5357
/**
@@ -57,6 +61,15 @@ export function useWorkflowKeyboard(options: UseWorkflowKeyboardOptions) {
5761
useKeyboard((evt) => {
5862
// === GLOBAL SHORTCUTS (always work) ===
5963

64+
// Shift+Tab - disable autonomous mode
65+
if (evt.shift && evt.name === "tab") {
66+
evt.preventDefault()
67+
if (options.isAutonomousMode?.()) {
68+
options.disableAutonomousMode?.()
69+
}
70+
return
71+
}
72+
6073
// Ctrl+S - skip (ALWAYS available)
6174
// When waiting for input: skip remaining prompts
6275
// When running: skip current agent

src/cli/tui/routes/workflow/state/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export interface WorkflowState {
146146
workflowStatus: WorkflowStatus
147147
agentIdMapVersion: number
148148
agentLogs: Map<string, string[]>
149+
autonomousMode: boolean
149150
}
150151

151152
export type ThemeLike = {

src/cli/tui/routes/workflow/workflow-shell.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { useWorkflowModals } from "./hooks/use-workflow-modals"
2222
import { useWorkflowKeyboard } from "./hooks/use-workflow-keyboard"
2323
import { calculateVisibleItems } from "./constants"
2424
import type { WorkflowEventBus } from "../../../../workflows/events/index.js"
25+
import { setAutonomousMode as persistAutonomousMode, loadControllerConfig } from "../../../../shared/workflows/index.js"
26+
import path from "path"
2527

2628
export interface WorkflowShellProps {
2729
version: string
@@ -103,10 +105,18 @@ export function WorkflowShell(props: WorkflowShellProps) {
103105
setErrorMessage(null)
104106
}
105107

106-
onMount(() => {
108+
onMount(async () => {
107109
;(process as NodeJS.EventEmitter).on('workflow:error', handleWorkflowError)
108110
;(process as NodeJS.EventEmitter).on('workflow:stopping', handleStopping)
109111
;(process as NodeJS.EventEmitter).on('workflow:user-stop', handleUserStop)
112+
113+
// Load initial autonomous mode state
114+
const cmRoot = path.join(props.currentDir, '.codemachine')
115+
const controllerState = await loadControllerConfig(cmRoot)
116+
if (controllerState?.autonomousMode) {
117+
ui.actions.setAutonomousMode(true)
118+
}
119+
110120
if (props.eventBus) {
111121
adapter = new OpenTUIAdapter({ actions: ui.actions })
112122
adapter.connect(props.eventBus)
@@ -274,6 +284,17 @@ export function WorkflowShell(props: WorkflowShellProps) {
274284
;(process as NodeJS.EventEmitter).emit("workflow:pause")
275285
}
276286

287+
// Disable autonomous mode
288+
const disableAutonomousMode = () => {
289+
const cwd = props.currentDir
290+
const cmRoot = path.join(cwd, '.codemachine')
291+
ui.actions.setAutonomousMode(false)
292+
persistAutonomousMode(cmRoot, false).catch(() => {
293+
// best-effort persistence
294+
})
295+
toast.show({ variant: "warning", message: "Autonomous mode disabled", duration: 3000 })
296+
}
297+
277298
const getMonitoringId = (uiAgentId: string): number | undefined => {
278299
const s = state()
279300
const mainAgent = s.agents.find((a) => a.id === uiAgentId)
@@ -307,6 +328,8 @@ export function WorkflowShell(props: WorkflowShellProps) {
307328
canFocusPromptBox: () => isWaitingForInput() && isShowingRunningAgent() && !isPromptBoxFocused(),
308329
focusPromptBox: () => setIsPromptBoxFocused(true),
309330
exitPromptBoxFocus: () => setIsPromptBoxFocused(false),
331+
isAutonomousMode: () => state().autonomousMode,
332+
disableAutonomousMode,
310333
})
311334

312335
return (
@@ -343,8 +366,8 @@ export function WorkflowShell(props: WorkflowShellProps) {
343366
</box>
344367

345368
<box flexShrink={0} flexDirection="column">
346-
<TelemetryBar workflowName={state().workflowName} runtime={runtime()} status={state().workflowStatus} total={totalTelemetry()} />
347-
<StatusFooter />
369+
<TelemetryBar workflowName={state().workflowName} runtime={runtime()} status={state().workflowStatus} total={totalTelemetry()} autonomousMode={state().autonomousMode} />
370+
<StatusFooter autonomousMode={state().autonomousMode} />
348371
</box>
349372

350373
<Show when={isCheckpointActive()}>

0 commit comments

Comments
 (0)