mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
* fix: cancel backend auto-approval timeout when auto-approve is toggled off mid-countdown (#11439) Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local> * fix: prevent chat history loss during cloud/settings navigation (#11371) (#11372) Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local> * fix: preserve pasted images in chatbox during chat activity (#11375) Co-authored-by: Roo Code <roomote@roocode.com> * fix: resolve chat scroll anchoring and task-switch scroll race condit… (#11385) * fix: avoid zsh process-substitution false positives in assignments (#11365) * fix(editor): make tab close best-effort in DiffViewProvider.open (#11363) * fix(checkpoints): canonicalize core.worktree comparison to prevent Windows path mismatch failures (#11346) * fix: prevent double notification sound playback (#11283) * fix: prevent false unsaved changes prompt with OpenAI Compatible headers (#8230) (#11334) fix: prevent false unsaved changes prompt with OpenAI Compatible headers Mark automatic header syncs in ApiOptions and OpenAICompatible as non-user actions (isUserAction: false) and enhance SettingsView change detection to skip automatic syncs with semantically equal values. Root cause: two components (ApiOptions and OpenAICompatible) manage openAiHeaders state and automatically sync it back on mount/remount. These syncs were treated as user changes, triggering a false dirty state. Co-authored-by: Robert McIntyre <robertjmcintyre@users.noreply.github.com> * fix: remove noisy console.warn logs from NativeToolCallParser (#11264) Remove two console.warn messages that fire excessively when loading tasks from history: - 'Attempting to finalize unknown tool call' in finalizeStreamingToolCall() - 'Received chunk for unknown tool call' in processStreamingChunk() The defensive null-return behavior is preserved; only the log output is removed. * refactor: remove footgun prompting (file-based system prompt override) (#11387) * refactor: delete orphaned per-provider caching transform files (#11388) * feat: add disabledTools setting to globally disable native tools (#11277) * feat: add disabledTools setting to globally disable native tools Add a disabledTools field to GlobalSettings that allows disabling specific native tools by name. This enables cloud agents to be configured with restricted tool access. Schema: - Add disabledTools: z.array(toolNamesSchema).optional() to globalSettingsSchema - Add disabledTools to organizationDefaultSettingsSchema.pick() - Add disabledTools to ExtensionState Pick type Prompt generation (tool filtering): - Add disabledTools to BuildToolsOptions interface - Pass disabledTools through filterSettings to filterNativeToolsForMode() - Remove disabled tools from allowedToolNames set in filterNativeToolsForMode() Execution-time validation (safety net): - Extract disabledTools from state in presentAssistantMessage - Convert disabledTools to toolRequirements format for validateToolUse() Wiring: - Add disabledTools to ClineProvider getState() and getStateToPostToWebview() - Pass disabledTools to all buildNativeToolsArrayWithRestrictions() call sites EXT-778 * fix: check toolRequirements before ALWAYS_AVAILABLE_TOOLS Moves the toolRequirements check before the ALWAYS_AVAILABLE_TOOLS early-return in isToolAllowedForMode(). This ensures disabledTools can block always-available tools (switch_mode, new_task, etc.) at execution time, making the validation layer consistent with the filtering layer. * feat: add support for .agents/skills directory (#11181) * feat: add support for .agents/skills directory This change adds support for discovering skills from the .agents/skills directory, following the Agent Skills convention for sharing skills across different AI coding tools. Priority order (later entries override earlier ones): 1. Global ~/.agents/skills (shared across AI coding tools, lowest priority) 2. Project .agents/skills 3. Global ~/.roo/skills (Roo-specific) 4. Project .roo/skills (highest priority) Changes: - Add getGlobalAgentsDirectory() and getProjectAgentsDirectoryForCwd() functions to roo-config - Update SkillsManager.getSkillsDirectories() to include .agents/skills - Update SkillsManager.setupFileWatchers() to watch .agents/skills - Add tests for new functionality * fix: clarify skill priority comment to match actual behavior * fix: clarify skill priority comment to explain Map.set replacement mechanism --------- Co-authored-by: Roo Code <roomote@roocode.com> * feat(history): render nested subtasks as recursive tree (#11299) * feat(history): render nested subtasks as recursive tree * fix(lockfile): resolve missing ai-sdk provider entry * fix: address review feedback — dedupe countAll, increase SubtaskRow max-h - HistoryView: replace local countAll with imported countAllSubtasks from types.ts - SubtaskRow: increase nested children max-h from 500px to 2000px to match TaskGroupItem * perf(refactor): consolidate getState calls in resolveWebviewView (#11320) * perf(refactor): consolidate getState calls in resolveWebviewView Replace three separate this.getState().then() calls with a single await this.getState() and destructuring. This avoids running the full getState() method (CloudService calls, ContextProxy reads, etc.) three times during webview view resolution. * fix: keep getState consolidation non-blocking to avoid delaying webview render --------- Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com> * fix: harden command auto-approval against inline JS false positives (#11382) * feat: rename search_and_replace tool to edit and unify edit-family UI (#11296) * Revert "refactor: delete orphaned per-provider caching transform files (#11388)" This reverts commit13a45b0361. * chore: regenerate built-in-skills.ts with updated formatting * fix: add missing maxReadFileLine property to test baseState The ExtensionState type now requires maxReadFileLine property (added in commit63e3f769a). Update the test to include this property with the default value of -1 (unlimited reading). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: add pnpm serve command for code-server development (#10964) Co-authored-by: Roo Code <roomote@roocode.com> * chore: remove Feature Request from issue template options (#11141) Co-authored-by: Roo Code <roomote@roocode.com> * refactor(docs-extractor): simplify mode to focus on raw fact extraction (#11129) * Add cli support for linux (#11167) * fix: replace heredocs with echo statements in cli-release workflow (#11168) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * Drop MacOS-13 cli support (#11169) * fix(cli): correct example in install script (#11170) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * feat: add Kimi K2.5 model to Fireworks provider (#11177) * feat(cli): improve dev experience and roo provider API key support (#11203) - Allow --api-key and ROO_API_KEY env var for the roo provider instead of requiring cloud auth token - Switch dev/start scripts to use tsx for running directly from source without building first - Fix path resolution (version.ts, extension.ts, extension-host.ts) to work from both source and bundled locations - Disable debug log file (~/.roo/cli-debug.log) unless --debug is passed - Update README with complete env var table and dev workflow docs Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * Roo Code CLI v0.0.50 (#11204) * Roo Code CLI v0.0.50 * docs(cli): add --exit-on-error to changelog --------- Co-authored-by: Roo Code <roomote@roocode.com> * feat(cli): update default model from Opus 4.5 to Opus 4.6 (#11273) Co-authored-by: Roo Code <roomote@roocode.com> * feat(web): replace Roomote Control with Linear Integration in cloud features grid (#11280) Co-authored-by: Roo Code <roomote@roocode.com> * Add linux-arm64 for the roo cli (#11314) * chore: clean up repo-facing mode rules (#11410) * Make CLI auto-approve by default with require-approval opt-in (#11424) Co-authored-by: Roo Code <roomote@roocode.com> * Add new code owners to CODEOWNERS file * Update next.js (#11108) * feat(web): Replace bespoke navigation menu with shadcn navigation menu (#11117) Co-authored-by: Roo Code <roomote@roocode.com> --------- Co-authored-by: SannidhyaSah <sah_sannidhya@outlook.com> Co-authored-by: Sannidhya <sann@Sannidhyas-MacBook-Pro.local> Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Hannes Rudolph <hrudolph@gmail.com> Co-authored-by: 0xMink <dennis@dennismink.com> Co-authored-by: Robert McIntyre <robertjmcintyre@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com> Co-authored-by: Chris Estreich <cestreich@gmail.com>
356 lines
13 KiB
Markdown
356 lines
13 KiB
Markdown
# CLI Agent Loop
|
|
|
|
This document explains how the Roo Code CLI detects and tracks the agent loop state.
|
|
|
|
## Overview
|
|
|
|
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.
|
|
|
|
## The Message Model
|
|
|
|
All agent activity is communicated through **ClineMessages** - a stream of timestamped messages that represent everything the agent does.
|
|
|
|
### Message Structure
|
|
|
|
```typescript
|
|
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?
|
|
}
|
|
```
|
|
|
|
### Two Types of Messages
|
|
|
|
| Type | Purpose | Blocks Agent? |
|
|
| ------- | ---------------------------------------------- | ------------- |
|
|
| **say** | Informational - agent is telling you something | No |
|
|
| **ask** | Interactive - agent needs something from you | Usually yes |
|
|
|
|
## The Key Insight
|
|
|
|
> **The agent loop stops whenever the last message is an `ask` type (with `partial: false`).**
|
|
|
|
The specific `ask` value tells you exactly what the agent needs.
|
|
|
|
## Ask Categories
|
|
|
|
The CLI categorizes asks into four groups:
|
|
|
|
### 1. Interactive Asks → `WAITING_FOR_INPUT` state
|
|
|
|
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 |
|
|
|
|
### 2. Idle Asks → `IDLE` state
|
|
|
|
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 |
|
|
|
|
### 3. Resumable Asks → `RESUMABLE` state
|
|
|
|
| Ask Type | What It Means | Response Options |
|
|
| ------------- | ------------------------- | ----------------- |
|
|
| `resume_task` | Task paused mid-execution | Resume or abandon |
|
|
|
|
### 4. Non-Blocking Asks → `RUNNING` state
|
|
|
|
| Ask Type | What It Means | Response Options |
|
|
| ---------------- | ------------------ | ----------------- |
|
|
| `command_output` | Command is running | Continue or abort |
|
|
|
|
## Streaming Detection
|
|
|
|
The agent is **streaming** when:
|
|
|
|
1. **`partial: true`** on the last message, OR
|
|
2. **An `api_req_started` message exists** with `cost: undefined` in its text field
|
|
|
|
```typescript
|
|
// 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
|
|
}
|
|
```
|
|
|
|
## State Machine
|
|
|
|
```
|
|
┌─────────────────┐
|
|
│ NO_TASK │ (no messages)
|
|
└────────┬────────┘
|
|
│ newTask
|
|
▼
|
|
┌─────────────────────────────┐
|
|
┌───▶│ RUNNING │◀───┐
|
|
│ └──────────┬──────────────────┘ │
|
|
│ │ │
|
|
│ ┌──────────┼──────────────┐ │
|
|
│ │ │ │ │
|
|
│ ▼ ▼ ▼ │
|
|
│ ┌──────┐ ┌─────────┐ ┌──────────┐ │
|
|
│ │STREAM│ │WAITING_ │ │ IDLE │ │
|
|
│ │ ING │ │FOR_INPUT│ │ │ │
|
|
│ └──┬───┘ └────┬────┘ └────┬─────┘ │
|
|
│ │ │ │ │
|
|
│ │ done │ approved │ newTask │
|
|
└────┴───────────┴────────────┘ │
|
|
│
|
|
┌──────────────┐ │
|
|
│ RESUMABLE │────────────────────────┘
|
|
└──────────────┘ resumed
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 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) │ │
|
|
│ └────────────────┘ └────────────────┘ └────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Component Responsibilities
|
|
|
|
### ExtensionClient
|
|
|
|
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)
|
|
|
|
```typescript
|
|
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}`)
|
|
})
|
|
```
|
|
|
|
### StateStore
|
|
|
|
Holds the `clineMessages` array, computed state, and current mode:
|
|
|
|
```typescript
|
|
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")
|
|
}
|
|
```
|
|
|
|
### MessageProcessor
|
|
|
|
Handles incoming messages from the extension:
|
|
|
|
- `"state"` messages → Update `clineMessages` array and track mode
|
|
- `"messageUpdated"` messages → Update single message in array
|
|
- Emits events for state transitions and mode changes
|
|
|
|
### AskDispatcher
|
|
|
|
Routes asks to appropriate handlers:
|
|
|
|
- Uses type guards: `isIdleAsk()`, `isInteractiveAsk()`, etc.
|
|
- Coordinates between `OutputManager` and `PromptManager`
|
|
- By default, the CLI auto-approves tool/command/browser/MCP actions
|
|
- In `--require-approval` mode, those actions prompt for manual approval
|
|
|
|
### OutputManager
|
|
|
|
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)
|
|
|
|
### PromptManager
|
|
|
|
Handles user input:
|
|
|
|
- Yes/no prompts
|
|
- Text input prompts
|
|
- Timed prompts with auto-defaults
|
|
|
|
## Response Messages
|
|
|
|
When the agent is waiting, send these responses:
|
|
|
|
```typescript
|
|
// 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",
|
|
})
|
|
```
|
|
|
|
## Type Guards
|
|
|
|
The CLI uses type guards from `@roo-code/types` for categorization:
|
|
|
|
```typescript
|
|
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
|
|
}
|
|
```
|
|
|
|
## Debug Logging
|
|
|
|
Enable with `-d` flag. Logs go to `~/.roo/cli-debug.log`:
|
|
|
|
```bash
|
|
roo -d -P "Build something" --no-tui
|
|
```
|
|
|
|
View logs:
|
|
|
|
```bash
|
|
tail -f ~/.roo/cli-debug.log
|
|
```
|
|
|
|
Example 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 }
|
|
```
|
|
|
|
## Summary
|
|
|
|
1. **Agent communicates via `ClineMessage` stream**
|
|
2. **Last message determines state**
|
|
3. **`ask` messages (non-partial) block the agent**
|
|
4. **Ask category determines required action**
|
|
5. **`partial: true` or `api_req_started` without cost = streaming**
|
|
6. **`ExtensionClient` is the single source of truth**
|