This document explains how the Roo Code CLI detects and tracks the agent loop state.
The CLI needs to know when the agent is:
- Running (actively processing)
- Streaming (receiving content from the API)
- Waiting for input (needs user approval or answer)
- Idle (task completed or failed)
This is accomplished by analyzing the messages the extension sends to the client.
All agent activity is communicated through ClineMessages - a stream of timestamped messages that represent everything the agent does.
interface ClineMessage {
ts: number // Unique timestamp identifier
type: "ask" | "say" // Message category
ask?: ClineAsk // Specific ask type (when type="ask")
say?: ClineSay // Specific say type (when type="say")
text?: string // Message content
partial?: boolean // Is this message still streaming?
}| Type | Purpose | Blocks Agent? |
|---|---|---|
| say | Informational - agent is telling you something | No |
| ask | Interactive - agent needs something from you | Usually yes |
The agent loop stops whenever the last message is an
asktype (withpartial: false).
The specific ask value tells you exactly what the agent needs.
The CLI categorizes asks into four groups:
These require user action to continue:
| Ask Type | What It Means | Required Response |
|---|---|---|
tool |
Wants to edit/create/delete files | Approve or Reject |
command |
Wants to run a terminal command | Approve or Reject |
followup |
Asking a question | Text answer |
browser_action_launch |
Wants to use the browser | Approve or Reject |
use_mcp_server |
Wants to use an MCP server | Approve or Reject |
These indicate the task has stopped:
| Ask Type | What It Means | Response Options |
|---|---|---|
completion_result |
Task completed successfully | New task or feedback |
api_req_failed |
API request failed | Retry or new task |
mistake_limit_reached |
Too many errors | Continue anyway or new task |
auto_approval_max_req_reached |
Auto-approval limit hit | Continue manually or stop |
resume_completed_task |
Viewing completed task | New task |
| Ask Type | What It Means | Response Options |
|---|---|---|
resume_task |
Task paused mid-execution | Resume or abandon |
| Ask Type | What It Means | Response Options |
|---|---|---|
command_output |
Command is running | Continue or abort |
The agent is streaming when:
partial: trueon the last message, OR- An
api_req_startedmessage exists withcost: undefinedin its text field
// Streaming detection pseudocode
function isStreaming(messages) {
const lastMessage = messages.at(-1)
// Check partial flag (primary indicator)
if (lastMessage?.partial === true) {
return true
}
// Check for in-progress API request
const apiReq = messages.findLast((m) => m.say === "api_req_started")
if (apiReq?.text) {
const data = JSON.parse(apiReq.text)
if (data.cost === undefined) {
return true // API request not yet complete
}
}
return false
} ┌─────────────────┐
│ NO_TASK │ (no messages)
└────────┬────────┘
│ newTask
▼
┌─────────────────────────────┐
┌───▶│ RUNNING │◀───┐
│ └──────────┬──────────────────┘ │
│ │ │
│ ┌──────────┼──────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────┐ ┌─────────┐ ┌──────────┐ │
│ │STREAM│ │WAITING_ │ │ IDLE │ │
│ │ ING │ │FOR_INPUT│ │ │ │
│ └──┬───┘ └────┬────┘ └────┬─────┘ │
│ │ │ │ │
│ │ done │ approved │ newTask │
└────┴───────────┴────────────┘ │
│
┌──────────────┐ │
│ RESUMABLE │────────────────────────┘
└──────────────┘ resumed
┌─────────────────────────────────────────────────────────────────┐
│ ExtensionHost │
│ │
│ ┌──────────────────┐ │
│ │ Extension │──── extensionWebviewMessage ─────┐ │
│ │ (Task.ts) │ │ │
│ └──────────────────┘ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ ExtensionClient │ │
│ │ (Single Source of Truth) │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌────────────────────┐ │ │
│ │ │ MessageProcessor │───▶│ StateStore │ │ │
│ │ │ │ │ (clineMessages) │ │ │
│ │ └─────────────────┘ └────────┬───────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ detectAgentState() │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Events: stateChange, message, waitingForInput, etc. │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ OutputManager │ │ AskDispatcher │ │ PromptManager │ │
│ │ (stdout) │ │ (ask routing) │ │ (user input) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
The single source of truth for agent state, including the current mode. It:
- Receives all messages from the extension
- Stores them in the
StateStore - Tracks the current mode from state messages
- Computes the current state via
detectAgentState() - Emits events when state changes (including mode changes)
const client = new ExtensionClient({
sendMessage: (msg) => extensionHost.sendToExtension(msg),
debug: true, // Writes to ~/.roo/cli-debug.log
})
// Query state at any time
const state = client.getAgentState()
if (state.isWaitingForInput) {
console.log(`Agent needs: ${state.currentAsk}`)
}
// Query current mode
const mode = client.getCurrentMode()
console.log(`Current mode: ${mode}`) // e.g., "code", "architect", "ask"
// Subscribe to events
client.on("waitingForInput", (event) => {
console.log(`Waiting for: ${event.ask}`)
})
// Subscribe to mode changes
client.on("modeChanged", (event) => {
console.log(`Mode changed: ${event.previousMode} -> ${event.currentMode}`)
})Holds the clineMessages array, computed state, and current mode:
interface StoreState {
messages: ClineMessage[] // The raw message array
agentState: AgentStateInfo // Computed state
isInitialized: boolean // Have we received any state?
currentMode: string | undefined // Current mode (e.g., "code", "architect")
}Handles incoming messages from the extension:
"state"messages → UpdateclineMessagesarray and track mode"messageUpdated"messages → Update single message in array- Emits events for state transitions and mode changes
Routes asks to appropriate handlers:
- Uses type guards:
isIdleAsk(),isInteractiveAsk(), etc. - Coordinates between
OutputManagerandPromptManager - By default, the CLI auto-approves tool/command/browser/MCP actions
- In
--require-approvalmode, those actions prompt for manual approval
Handles all CLI output:
- Streams partial content with delta computation
- Tracks what's been displayed to avoid duplicates
- Writes directly to
process.stdout(bypasses quiet mode)
Handles user input:
- Yes/no prompts
- Text input prompts
- Timed prompts with auto-defaults
When the agent is waiting, send these responses:
// Approve an action (tool, command, browser, MCP)
client.sendMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
})
// Reject an action
client.sendMessage({
type: "askResponse",
askResponse: "noButtonClicked",
})
// Answer a question
client.sendMessage({
type: "askResponse",
askResponse: "messageResponse",
text: "My answer here",
})
// Start a new task
client.sendMessage({
type: "newTask",
text: "Build a web app",
})
// Cancel current task
client.sendMessage({
type: "cancelTask",
})The CLI uses type guards from @roo-code/types for categorization:
import { isIdleAsk, isInteractiveAsk, isResumableAsk, isNonBlockingAsk } from "@roo-code/types"
const ask = message.ask
if (isInteractiveAsk(ask)) {
// Needs approval: tool, command, followup, etc.
} else if (isIdleAsk(ask)) {
// Task stopped: completion_result, api_req_failed, etc.
} else if (isResumableAsk(ask)) {
// Task paused: resume_task
} else if (isNonBlockingAsk(ask)) {
// Command running: command_output
}Enable with -d flag. Logs go to ~/.roo/cli-debug.log:
zoo -d -P "Build something" --no-tuiView logs:
tail -f ~/.roo/cli-debug.logExample output:
[MessageProcessor] State update: {
"messageCount": 5,
"lastMessage": {
"msgType": "ask:completion_result"
},
"stateTransition": "running → idle",
"currentAsk": "completion_result",
"isWaitingForInput": true
}
[MessageProcessor] EMIT waitingForInput: { "ask": "completion_result" }
[MessageProcessor] EMIT taskCompleted: { "success": true }
- Agent communicates via
ClineMessagestream - Last message determines state
askmessages (non-partial) block the agent- Ask category determines required action
partial: trueorapi_req_startedwithout cost = streamingExtensionClientis the single source of truth