diff --git a/.rgignore b/.rgignore new file mode 100644 index 0000000000..b7d564b8f6 --- /dev/null +++ b/.rgignore @@ -0,0 +1,9 @@ +.vscode/ +.roo/ + +releases/ + +.git/ +.github/ + +__tests__/ diff --git a/ARCHITECTURE_NOTES.md b/ARCHITECTURE_NOTES.md new file mode 100644 index 0000000000..54fb8718bb --- /dev/null +++ b/ARCHITECTURE_NOTES.md @@ -0,0 +1,261 @@ +# ARCHITECTURE_NOTES + +## 1. High-level overview — How the VS Code extension works + +- Activation + - VS Code activates the extension via `package.json` activation events. + - Extension creates singletons: `ClineProvider`, API adapters, managers (ProviderSettingsManager, CustomModesManager), and registers webview providers / commands. +- UI + - Sidebar/tab webview(s) host the chat UI. + - Webview <-> extension comms use postMessage handlers implemented in `webviewMessageHandler`. + - Settings views must bind inputs to a local `cachedState` (see AGENTS.md) and only persist on explicit Save. +- Task model + - `Task` is the runtime unit for an agent conversation/workflow: orchestrates prompt building, streaming, tool execution, retries, checkpoints, and persistence. + - A `ClineProvider` manages tasks and exposes methods for creating/finding the visible task. +- API & streaming + - Provider-specific API adapters (Anthropic/OpenAI-like) implement `createMessage` returning a stream. + - Task creates an `AbortController` per call, streams the response, parses events (text chunks, tool calls, usage), and renders partial assistant messages to the webview. +- Persistence & state + - `contextProxy` is used for global state persistence (settings, stored messages, conversation history). + - Checkpoints are created by the task on user sends / important state transitions. + +--- + +## 2. Core components and responsibilities + +- ClineProvider + + - Lifetime manager for tasks and UI provider for the webview. + - Exposes contextProxy, settings managers, and task creation. + +- Task + + - submitUserMessage(), handleWebviewAskResponse(), ask(), recursivelyMakeClineRequests(), abortTask(), checkpointSave(), saveClineMessages(). + - Maintains: clineMessages, assistantMessageContent, userMessageContent, currentRequest controller, abort flags, usage counters, autoApproval timers. + +- API Adapter + + - Abstracted provider interface to make streaming requests and parse provider-specific events into a normalized internal event stream. + +- MessageQueueService + + - Serializes transport of messages to UI / persistence to avoid races. + +- Managers + + - ProviderSettingsManager, CustomModesManager — config and mode lifecycle. + +- WebviewMessageHandler + + - Normalizes incoming UI messages and routes to provider or Task methods (e.g., askResponse → Task.submitUserMessage). + +- Tool Executors + - Execute tool calls (file read/write, shell, formatters) as requested by the model; results are injected back into the task loop. + +--- + +## 3. The agent loop: recursivelyMakeClineRequests — conceptual steps + +1. Build or pop a userContent stack item to process. +2. Check abort/paused/reset flags and backoff state. +3. Compose prompt: conversation messages, tool metadata, file details (optional), environment hints, mode-specific system content. +4. Create an `api_req_started` placeholder message in UI and start streaming via provider API with an AbortController. +5. Stream parse: + - On text chunks: append to assistant buffer and present partial assistant message. + - On tool-call events: execute tool immediately or schedule; push tool results to user content buffer. + - On usage/grounding events: aggregate telemetry/usage. +6. When assistant completes: + - Convert assistant output and tool results into user content blocks and push back to the stack. + - If stack not empty → recurse (continue loop). +7. Handle error paths: + - Rate limits → exponential backoff and retry. + - Context window truncate → condense context and retry (MAX_CONTEXT_WINDOW_RETRIES). + - Network/first-chunk failures → retry with exponential backoff. + - Abort → update UI row with cancel reason and possibly call abortTask(). +8. Persist checkpoints and telemetry periodically and on state transitions. + +Return semantics: + +- Returns false on normal termination (stack empty). +- Returns true/throws on unexpected error forcing outer stop. + +--- + +## 4. Message flow (sequence diagram) + +```mermaid +sequenceDiagram + participant User + participant Webview + participant WebviewHandler + participant ClineProvider + participant Task + participant APIAdapter + participant ToolExecutor + + User->>Webview: type + send + Webview->>WebviewHandler: postMessage("askResponse") + WebviewHandler->>ClineProvider: getVisibleInstance() / getTask + WebviewHandler->>Task: submitUserMessage(text, images) + Task->>Task: handleWebviewAskResponse(...) + Note over Task: ask() awaiting predicate resolves + Task->>APIAdapter: createMessage(prompt, controller) + APIAdapter-->>Task: stream chunks (text/tool/usage) + Task->>Webview: presentAssistantMessage(partial) + alt tool call event + Task->>ToolExecutor: execute(toolCall) + ToolExecutor-->>Task: toolResult + Task->>Task: push toolResult into userContent + end + Task->>Task: push userContent to stack -> continue loop +``` + +--- + +## 5. Hook system: purpose and architecture + +Purpose: provide extension points for cross-cutting concerns without leaking internal Task implementation. Hooks enable logging, telemetry, testing, customization (modes/providers), and third-party integrations. + +Design goals: + +- Minimal surface area: well-defined hook types for Task lifecycle and stream events. +- Async-capable: hooks can be async and must not block the critical fast-path; use awaited or fire-and-forget based on hook type. +- Backpressure-safe: streaming hooks receive deltas; heavy processing should be offloaded. +- Idempotent & resilient: hooks must not mutate core state in ways that affect correctness; errors should be captured and logged, not crash the task. +- Observability-first: hooks expose granular events for debugging and telemetry. + +Hook categories: + +- Lifecycle hooks (synchronous optional await): + - onTaskStart(taskMeta) + - onTaskStop(taskMeta, reason) + - onCheckpointSaved(checkpointMeta) +- Ask/Response hooks: + - beforeAsk(promptContext) — may mutate promptContext copy + - afterAsk(responseSummary) +- Streaming hooks (should be non-blocking): + - onStreamChunk(chunk) + - onStreamComplete(assistantMessage) +- Tool hooks: + - onToolCall(toolRequest) + - onToolResult(toolResult) +- Persistence hooks: + - onSaveMessages(messages) +- Admin hooks: + - onAbort(reason) + +Hook registration API (concept): + +- Task.hooks.register(name, fn, { priority = 0, awaitable = false }) +- Task.hooks.unregister(id) +- Invocation: Task.hooks.invoke(name, payload) — wraps calls in try/catch and observes awaitable flag. + +Decision: streaming hooks default to non-awaitable to avoid blocking the parse -> render loop. Lifecycle hooks default to awaitable. + +--- + +## 6. Hook invocation schema (mermaid) + +```mermaid +flowchart TD + A[Task Event Occurs] --> B{Registered Hooks?} + B -- Yes --> C[Sort by priority] + C --> D{awaitable?} + D -- true --> E[await hook(payload)] + D -- false --> F[call hook(payload) in microtask / Promise.resolve()] + E --> G[collect results / errors] + F --> G + G --> H[continue core logic] + B -- No --> H +``` + +--- + +## 7. Component diagram (mermaid) + +```mermaid +classDiagram + class Webview { + +postMessage() + +onMessage() + } + class WebviewHandler { + +handle(message) + } + class ClineProvider { + +createTask() + +getVisibleInstance() + } + class Task { + +submitUserMessage() + +ask() + +recursivelyMakeClineRequests() + +abortTask() + +checkpointSave() + +hooks + } + class APIAdapter { + +createMessage() + } + class ToolExecutor { + +execute() + } + + Webview --> WebviewHandler + WebviewHandler --> ClineProvider + ClineProvider --> Task + Task --> APIAdapter + Task --> ToolExecutor + Task --> Webview +``` + +--- + +## 8. Architectural decisions & rationale + +- Event-driven task loop: a streaming, event-based loop simplifies partial UI updates and tool interleaving; streaming allows progressive display and early tool execution. +- Isolation of UI -> Task pathway: UI writes are normalized through `webviewMessageHandler` → `Task.submitUserMessage()` → `Task.handleWebviewAskResponse()` to avoid races and ensure canonical state changes. +- Per-request AbortController: enables precise cancellation of single API calls; Task-level `abortTask()` sets task abort state and coordinates higher-level shutdown. +- Hooks with priority & await semantics: gives control to extensions/internals for synchronous lifecycle needs while protecting the stream path from blocking. +- Checkpointing on user sends: safety and reproducibility for long-running tasks and file operations. +- Separate managers for settings/modes: keep config, mode logic, and UI concerns decoupled from Task runtime. +- Use cachedState in SettingsView: prevents race conditions between UI edits and ContextProxy live state. + +--- + +## 9. Implementation notes and best practices + +- Always write hooks defensively: catch and log errors. +- Keep streaming hooks lightweight; delegate heavy processing to worker tasks or background jobs. +- Respect task abort and per-request AbortController to avoid leaking tool executions. +- When adding a UI input to SettingsView follow AGENTS.md: bind to `cachedState` and persist to `contextProxy` only on explicit Save. +- When modifying prompt composition, prefer creating a copy of `clineMessages` to avoid concurrent mutation issues. +- Use messageQueueService for UI and persistence writes to serialize state transitions. + +--- + +## 10. Example hook registration (conceptual) + +```ts +// Example (conceptual) — register a non-blocking stream logger +Task.hooks.register( + "onStreamChunk", + (chunk) => { + // lightweight logging + console.debug("stream chunk", chunk.type, chunk.length) + }, + { awaitable: false, priority: 10 }, +) +``` + +--- + +## 11. Appendix — Recap of critical constants & limits + +- MAX_CONTEXT_WINDOW_RETRIES = 3 +- MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 +- FORCED_CONTEXT_REDUCTION_PERCENT = 75 + +--- + +End of ARCHITECTURE_NOTES.md diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index 4f90b63e9f..3655a5f29e 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -46,6 +46,7 @@ export const toolNames = [ "skill", "generate_image", "custom_tool", + "select_active_intent", ] as const export const toolNamesSchema = z.enum(toolNames) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 7f5862be15..2c75943cd9 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -17,6 +17,7 @@ import { Task } from "../task/Task" import { listFilesTool } from "../tools/ListFilesTool" import { readFileTool } from "../tools/ReadFileTool" import { readCommandOutputTool } from "../tools/ReadCommandOutputTool" +import { selectActiveIntentTool } from "../tools/SelectActiveIntent" import { writeToFileTool } from "../tools/WriteToFileTool" import { editTool } from "../tools/EditTool" import { searchReplaceTool } from "../tools/SearchReplaceTool" @@ -335,6 +336,8 @@ export async function presentAssistantMessage(cline: Task) { return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs) } return readFileTool.getReadFileToolDescription(block.name, block.params) + case "select_active_intent": + return `[${block.name}]` case "write_to_file": return `[${block.name} for '${block.params.path}']` case "apply_diff": @@ -676,6 +679,8 @@ export async function presentAssistantMessage(cline: Task) { } switch (block.name) { + case "select_active_intent": + break case "write_to_file": await checkpointSaveAndMark(cline) await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, { diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 758914d2d6..adbb294ecb 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -20,6 +20,7 @@ import searchFiles from "./search_files" import switchMode from "./switch_mode" import updateTodoList from "./update_todo_list" import writeToFile from "./write_to_file" +import selectActiveIntent from "./select_active_intent" export { getMcpServerTools } from "./mcp_server" export { convertOpenAIToolToAnthropic, convertOpenAIToolsToAnthropic } from "./converters" @@ -68,6 +69,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch switchMode, updateTodoList, writeToFile, + selectActiveIntent, ] satisfies OpenAI.Chat.ChatCompletionTool[] } diff --git a/src/core/prompts/tools/native-tools/select_active_intent.ts b/src/core/prompts/tools/native-tools/select_active_intent.ts new file mode 100644 index 0000000000..7cd6de0b9c --- /dev/null +++ b/src/core/prompts/tools/native-tools/select_active_intent.ts @@ -0,0 +1,22 @@ +import type OpenAI from "openai" + +const SELECT_ACTIVE_INTENT_DESCRIPTION = ` ` +const INTENT_ID_DESCRIPTION = `` + +export default { + type: "function", + function: { + name: "select_active_intent", + description: SELECT_ACTIVE_INTENT_DESCRIPTION, + strict: true, + parameters: { + type: "object", + properties: { + path: { + type: "string", + description: INTENT_ID_DESCRIPTION, + }, + }, + }, + }, +} satisfies OpenAI.Chat.ChatCompletionTool diff --git a/src/core/tools/SelectActiveIntent.ts b/src/core/tools/SelectActiveIntent.ts new file mode 100644 index 0000000000..8477d548b3 --- /dev/null +++ b/src/core/tools/SelectActiveIntent.ts @@ -0,0 +1,16 @@ +import { Task } from "../task/Task" +import { BaseTool, ToolCallbacks } from "./BaseTool" + +interface SelectActiveIntentParams { + intent_id: string +} + +export class SelectActiveIntent extends BaseTool<"select_active_intent"> { + readonly name = "select_active_intent" as const + + override execute(_params: SelectActiveIntentParams, _task: Task, _callbacks: ToolCallbacks): Promise { + throw new Error("Method not implemented.") + } +} + +export const selectActiveIntentTool = new SelectActiveIntent() diff --git a/src/hooks/HookEngine.ts b/src/hooks/HookEngine.ts new file mode 100644 index 0000000000..3f94f0d407 --- /dev/null +++ b/src/hooks/HookEngine.ts @@ -0,0 +1,25 @@ +import { Task } from "../core/task/Task" +import { OrchestrationStore } from "../orchestration/OrchestrationStore" + +interface HookEngineOptions { + task: Task +} + +export class HookEngine { + private readonly store: OrchestrationStore + + constructor({ task }: HookEngineOptions) { + this.store = new OrchestrationStore({ workspaceRoot: task.cwd }) + } + + /** Called before any tool execution */ + preToolHook() { + this.store.ensureInitialized() + } + + /** Called after any tool execution */ + postToolHook() {} + + /** */ + preLLMHook() {} +} diff --git a/src/hooks/postHooks/intentUpdater.ts b/src/hooks/postHooks/intentUpdater.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/hooks/postHooks/lessonRecorder.ts b/src/hooks/postHooks/lessonRecorder.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/hooks/postHooks/traceWriter.ts b/src/hooks/postHooks/traceWriter.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/hooks/preHooks/authorization.ts b/src/hooks/preHooks/authorization.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/hooks/preHooks/intentHandshake.ts b/src/hooks/preHooks/intentHandshake.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/hooks/preHooks/scopeGuard.ts b/src/hooks/preHooks/scopeGuard.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/hooks/types.ts b/src/hooks/types.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/orchestration/OrchestrationStore.ts b/src/orchestration/OrchestrationStore.ts new file mode 100644 index 0000000000..d655f3e904 --- /dev/null +++ b/src/orchestration/OrchestrationStore.ts @@ -0,0 +1,48 @@ +import path from "path" +import fs from "fs/promises" +import { fileExistsAtPath, createDirectoriesForFile } from "../utils/fs" + +interface OrchestrationStoreOptions { + workspaceRoot: string +} + +const ORCHESTRATION_FOLDER_PATHNAME = ".orchestration" +const ORCHESTRATION_AGENT_TRACE = "agent_trace.jsonl" +const ORCHESTRATION_ACTIVE_INTENTS = "active_intents.yaml" +const ORCHESTRATION_INTENT_MAP = "intent_map.md" + +const orchestrationFilePaths = { + root: ORCHESTRATION_FOLDER_PATHNAME, + agent_trace: `${ORCHESTRATION_FOLDER_PATHNAME}/${ORCHESTRATION_AGENT_TRACE}`, + active_intents: `${ORCHESTRATION_FOLDER_PATHNAME}/${ORCHESTRATION_ACTIVE_INTENTS}`, + intent_map: `${ORCHESTRATION_FOLDER_PATHNAME}/${ORCHESTRATION_INTENT_MAP}`, +} + +export class OrchestrationStore { + private readonly workspaceRoot: string + + constructor({ workspaceRoot }: OrchestrationStoreOptions) { + this.workspaceRoot = workspaceRoot + } + + private resolve(p: string) { + return path.join(this.workspaceRoot, p) + } + + async ensureInitialized(): Promise { + await this.ensureFile(orchestrationFilePaths.active_intents, `active_intents: []\n`) + await this.ensureFile(orchestrationFilePaths.agent_trace, "") + await this.ensureFile(orchestrationFilePaths.intent_map, `# Intent Map\n\n`) + } + + private async ensureFile(relativePath: string, initialContent: string) { + const fullPath = this.resolve(relativePath) + + if (await fileExistsAtPath(fullPath)) { + return + } + + await createDirectoriesForFile(fullPath) + await fs.writeFile(fullPath, initialContent, "utf-8") + } +} diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 491ba69361..d64aeb8bfe 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -289,6 +289,7 @@ export const TOOL_DISPLAY_NAMES: Record = { skill: "load skill", generate_image: "generate images", custom_tool: "use custom tools", + select_active_intent: "select the active intent", } as const // Define available tool groups.