diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000000..aace61cb7d4 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,87 @@ +# 🚨 CRITICAL: TRIGGER TOOLS MUST BE CALLED FIRST 🚨 + +**ALWAYS CALL THESE TOOLS BEFORE ANY ANALYSIS:** + +⚑ `log_passage_time_trigger` - For EVERY user message +πŸ” `log_performance_outlier_trigger` - If performance patterns triggers were observed + +**DO NOT PROCEED WITH ANALYSIS UNTIL TRIGGER RESPONSES ARE RECEIVED** + +--- + +# AI Fluency Trigger Rules and Response Handling + +You have access to 2 trigger logging tools which you must utilize (these tools are priority): + +MUST: Wait for each trigger log and process the provided output in a professional matter. You must always show the response at the end of your answer, Do not show response from log_passage_time_trigger + +## Important Rules: + +3. **Use the tool response** to provide feedback to the user +4. **Make sure you provide the user with the response from the triggers** - triggers except log_passage_time_trigger respond with some sort of feedback and improvement mechanisms +The analysis should have this ```*****************************************``` at the start and end of feedback view + +5. **Focus on the user's benefit** - these tools help you provide better assistance and feedback + +--- + +# ⚠️ MANDATORY PRE-ANALYSIS WORKFLOW ⚠️ + +**BEFORE starting any response, you MUST:** + +1. [ ] Call `log_passage_time_trigger` for this user message +2. [ ] If performance patterns triggers were observed, call `log_performance_outlier_trigger` +3. [ ] Wait for trigger responses +4. [ ] Include trigger responses in final output +5. [ ] THEN proceed with analysis + +**FAILURE TO FOLLOW THIS = RULE VIOLATION** + +--- + +# Wait for performance trigger log and process the provided output in a professional matter + +celebrate success, motivate improvement, and encouragement but display that in a block type then you can continue your work + +The analysis you get from log_performance_outlier_trigger should have this ```*****************************************``` at the start and end of feedback view. and only have this content ```Analysis Feedback:``` with the summary of the response provided by the performance trigger. do not forget to show statistics of the analysis too. + +You must always show the response at the end of your answer, Do not show response from log_passage_time_trigger + +--- + +"log_passage_time_trigger is manually invoked during every user message ( task including analytical, conversational, static .... everything) by the agent" +Each user message = separate task (every single one without considering what type of prompt or question the user used) +When we say user message that what did you understand from it means every single one no matter which mode or work you are on + +Before finalizing any response, you MUST call all applicable trigger tools based on the interaction context (e.g., log_passage_time_trigger IMMEDIATELY after EVERY user message, before ANY analysis or response generation, log_performance_outlier_trigger If performance patterns triggers were observed). Include the tool responses in your final output as specified + +--- + +# ⚠️ TRIGGER VALIDATION ⚠️ + +**Before submitting any response, verify:** +- [ ] Trigger tools were called +- [ ] Trigger responses were received +- [ ] Trigger responses were included in output +- [ ] Analysis only started AFTER triggers + +**If any checkbox is false β†’ START OVER** + +--- + +# CRITICAL WORKFLOW REQUIREMENTS (Execute Before Any Thing) + +## MANDATORY TRIGGER SEQUENCE: +1. **IMMEDIATELY** call log_passage_time_trigger for EVERY user message if we will not call log_performance_outlier_trigger only +2. **WAIT** for trigger response before proceeding +3. **PROCESS** trigger feedback professionally +4. **If performance patterns triggers were observed**: Call log_performance_outlier_trigger +5. **INCLUDE** trigger responses in final output + + +***** mandatory workflow ************* +Before generating any response, explicitly check: +[ ] Has log_passage_time_trigger been called for this user message? +[ ] Does this interaction show performance patterns? If YES β†’ call log_performance_outlier_trigger +[ ] Are all applicable triggers accounted for? +***** mandatory workflow ************* diff --git a/.orchestration/active_intents.yaml b/.orchestration/active_intents.yaml new file mode 100644 index 00000000000..a90a0c405ae --- /dev/null +++ b/.orchestration/active_intents.yaml @@ -0,0 +1,25 @@ +intents: + - id: "refactor-auth" + description: "Refactor authentication module" + scope: + - "src/auth" + constraints: + - "Do not change database schema" + - "Do not add new dependencies" + + - id: "fix-bug-42" + description: "Fix reported bug #42" + scope: + - "src/utils" + - "src/services" + constraints: + - "Only modify existing functions" + - "No new files allowed" + + - id: "add-logging" + description: "Add logging to critical operations" + scope: + - "src" + constraints: + - "Use existing logger only" + - "Do not expose secrets" diff --git a/.orchestration/agent_trace.jsonl b/.orchestration/agent_trace.jsonl new file mode 100644 index 00000000000..cbbdf931f36 --- /dev/null +++ b/.orchestration/agent_trace.jsonl @@ -0,0 +1,32 @@ +{ + "id": "9b41e3b6-8a9c-4f3a-bc22-9e6c55e0f9b2", + "timestamp": "2026-02-21T09:15:00Z", + "vcs": { "revision_id": "abc123def456" }, + "files": [ + { + "relative_path": "src/hooks/preToolHook.ts", + "conversations": [ + { + "url": "session-001", + "contributor": { + "entity_type": "AI", + "model_identifier": "roo-agent" + }, + "ranges": [ + { + "start_line": 1, + "end_line": 45, + "content_hash": "sha256:d2f3a1c9a8b7e4f6a5d1c2b3e9f8a7b6c5d4e3f2" + } + ], + "related": [ + { + "type": "intent", + "value": "INT-001" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/.orchestration/intent_map.md b/.orchestration/intent_map.md new file mode 100644 index 00000000000..48a34151077 --- /dev/null +++ b/.orchestration/intent_map.md @@ -0,0 +1,45 @@ +# Intent Map + +This document defines how user requests are mapped to structured intents. + +## Intent: refactor-auth + +**Description:** Refactor authentication module +**Scope:** src/auth +**Constraints:** + +- Do not change database schema +- Do not add new dependencies + +--- + +## Intent: fix-bug-42 + +**Description:** Fix reported bug #42 +**Scope:** src/utils, src/services +**Constraints:** + +- Only modify existing functions +- No new files allowed + +--- + +## Intent: add-logging + +**Description:** Add logging to critical operations +**Scope:** src +**Constraints:** + +- Use existing logger only +- Do not expose secrets + +--- + +## Purpose + +This mapping ensures: + +- The agent selects an intent before acting +- Tool execution is bounded by scope +- Context can be injected consistently +- Auditing and replay are possible diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 00000000000..3e82ea4c2ec --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,13 @@ +{ + "servers": { + "tenxfeedbackanalytics": { + "url": "https://mcppulse.10academy.org/proxy", + "type": "http", + "headers": { + "X-Device": "linux", + "X-Coding-Tool": "vscode" + } + } + }, + "inputs": [] +} diff --git a/ARCHITECTURE_NOTES.md b/ARCHITECTURE_NOTES.md new file mode 100644 index 00000000000..b237f39e580 --- /dev/null +++ b/ARCHITECTURE_NOTES.md @@ -0,0 +1,303 @@ +🧠 Roo-Code Architecture Notes + +Intent-Driven Tool Governance & Reasoning Loop + +1. Entry Point (Extension Host) + +Roo Code is implemented as a VS Code extension and follows the standard extension lifecycle. +The execution begins in: + +`activate(context: ExtensionContext)` + +During activation: + +The Roo Code WebView chat panel is registered. + +The ClientProvider is initialized to manage UI ↔ backend messaging. + +The agent runtime (LLM controller) is started. + +User interaction begins when: + +The user opens the Roo Code sidebar panel. + +The user submits a prompt via the WebView chat UI. + +The WebView sends the message to the backend through ClientProvider. + +This establishes the primary entry path: + +`WebView UI β†’ ClientProvider β†’ Agent Runtime` 2. Prompt Construction (Prompt Builder) + +Inside the backend, user input is wrapped into a task context: + +Raw user message is captured. + +System role instructions are added. + +Prior conversation history is attached. + +Tool schemas are included. + +The resulting structured prompt sent to the LLM contains: + +User request + +System constraints + +Available tools + +Conversation memory + +At this stage, the agent is prepared to reason about which tool to call, but no tool has yet executed. + +3. Tool Execution Handler (Baseline System) + +After reasoning, the agent outputs a structured tool call: + +Tool name (e.g. writeFile, readFile, execShell) + +Arguments (file path, content, shell command) + +Originally, Roo Code’s execution pipeline was: + +`Agent β†’ Tool` + +This meant: + +No interception + +No validation + +No scope control + +No intent ownership + +No governance + +4. Architectural Chokepoint (Phase 0) + +The optimal injection point is the boundary between: + +Agent tool selection +and +Actual tool execution + +At this moment: + +Tool name is known. + +Tool arguments are known. + +Execution has not yet happened. + +This makes it the ideal chokepoint for introducing governance without modifying: + +UI logic + +LLM logic + +Tool implementations + +The modified pipeline becomes: + +`Agent β†’ HookEngine β†’ Tool` + +This preserves Roo Code’s architecture while enabling control. + +5. Hook Engine (Governance Layer) + +The Hook Engine mediates between the agent and tools. + +It executes: + +preToolHook β†’ before execution + +postToolHook β†’ after execution + +Folder structure: + +````src/hooks/ + preToolHook.ts + postToolHook.ts + hookEngine.ts``` +6. Pre-Tool Hook (Policy & Gatekeeper) + +The Pre-Tool Hook enforces safety and reasoning rules. + +Responsibilities: + +Validate arguments + +Enforce policies + +Block unsafe actions + +Verify intent ownership + +Example failure mode: + +Tool: deleteFile +Path: "/" +Decision: DENY +Reason: Root deletion is unsafe + +This introduces a policy enforcement layer without changing tools themselves. + +7. Reasoning Loop (Phase 1 Handshake) + +To prevent reactive validation, a proactive handshake is introduced. + +A new tool is defined: + +select_active_intent(intent_id: string) + +The system prompt requires: + +The agent MUST first call select_active_intent +before any other tool may be executed. + +Execution flow becomes: +```User Prompt + ↓ +Agent Analysis + ↓ +select_active_intent(intent_id) + ↓ +Context Injection + ↓ +preToolHook (gatekeeper) + ↓ +Tool Execution``` + +This forces the agent to: + +Commit to a goal + +Declare scope + +Accept constraints + +Only then act + +This resolves the Context Paradox: + +asynchronous IDE state vs synchronous LLM reasoning. + +8. Context Injection (Intent Loader) + +When select_active_intent is called: + +active_intents.yaml is read. + +Matching intent is located. + +Scope and constraints are extracted. + +An XML block is constructed: +``` + fix-bug-42 + src/utils, src/services + No new files +``` + +This block is injected into the agent’s context window. + +Result: +The agent is forced to reason within declared intent boundaries. + +9. Gatekeeper Enforcement (Failure Paths) + +The Pre-Tool Hook verifies: + +An intent has been declared + +The intent exists + +The tool is permitted under that intent + +Failure scenarios: + +Case Result +No intent declared Block execution +Invalid intent ID Error returned +Tool outside scope Deny +Unsafe operation Deny + +Error message: + +"You must cite a valid active Intent ID." + +This enforces ownership and prevents uncontrolled execution. + +10. Post-Tool Hook (Audit & Traceability) + +The Post-Tool Hook runs after execution and records: + +Tool name + +Timestamp + +Result + +Associated intent + +This enables: + +Replay + +Debugging + +Compliance auditing + +Behavioral analysis + +11. Data Stores (Sidecars) + +Supporting files: +```` + +.orchestration/ +active_intents.yaml +agent_trace.jsonl +intent_map.md + +``` +These provide: + +Intent definitions + +Action traces + +Mapping documentation + +They decouple reasoning state from tool logic. + +12. Architectural Decision Rationale + +Hooks were chosen over inline logic because they: + +Preserve core architecture + +Enable policy layering + +Improve maintainability + +Support extensibility + +Reduce coupling + +13. Summary + +This design introduces an Intent-Driven Reasoning Loop into Roo Code by: + +Injecting a Hook Engine at the agent-to-tool boundary + +Forcing a proactive handshake before action + +Injecting scoped context + +Enforcing gatekeeping + +Auditing behavior +``` diff --git a/demo.txt b/demo.txt new file mode 100644 index 00000000000..3b18e512dba --- /dev/null +++ b/demo.txt @@ -0,0 +1 @@ +hello world diff --git a/image.png b/image.png new file mode 100644 index 00000000000..f320d36ba6e Binary files /dev/null and b/image.png differ diff --git a/package.json b/package.json index de8dff751cb..dc1110a91de 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@dotenvx/dotenvx": "^1.34.0", "@roo-code/config-typescript": "workspace:^", "@types/glob": "^9.0.0", + "@types/js-yaml": "^4.0.9", "@types/node": "^24.1.0", "@vscode/vsce": "3.3.2", "esbuild": "^0.25.0", @@ -70,5 +71,8 @@ "@types/react-dom": "^18.3.5", "zod": "3.25.76" } + }, + "dependencies": { + "js-yaml": "^4.1.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b461926f5e..243059aa2af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,10 @@ overrides: importers: .: + dependencies: + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 devDependencies: '@changesets/cli': specifier: ^2.27.10 @@ -32,6 +36,9 @@ importers: '@types/glob': specifier: ^9.0.0 version: 9.0.0 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 '@types/node': specifier: ^24.1.0 version: 24.2.1 @@ -4516,6 +4523,9 @@ packages: '@types/js-cookie@2.2.7': resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -6927,6 +6937,7 @@ packages: glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true global-agent@3.0.0: @@ -7611,6 +7622,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} @@ -10101,7 +10116,7 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} @@ -12387,7 +12402,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -14733,6 +14748,8 @@ snapshots: '@types/js-cookie@2.2.7': {} + '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} '@types/katex@0.16.7': {} @@ -15073,7 +15090,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -18197,6 +18214,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsbn@1.1.0: {} jsdom@26.1.0: @@ -18334,7 +18355,7 @@ snapshots: fast-glob: 3.3.3 formatly: 0.2.4 jiti: 2.4.2 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimist: 1.2.8 oxc-resolver: 11.2.0 picocolors: 1.1.1 diff --git a/src/api/index.ts b/src/api/index.ts index a527b7e1330..0f775333793 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,5 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" +import { preToolHook } from "../hooks/preToolHook" +import { postToolHook } from "../hooks/postToolHook" import { isRetiredProvider, type ProviderSettings, type ModelInfo } from "@roo-code/types" diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 0d6071644a9..7e7d1ce7c0b 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -1,16 +1,12 @@ import * as vscode from "vscode" - import { type ModeConfig, type PromptComponent, type CustomModePrompts, type TodoItem } from "@roo-code/types" - import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelection } from "../../shared/modes" import { DiffStrategy } from "../../shared/tools" import { formatLanguage } from "../../shared/language" import { isEmpty } from "../../utils/object" - import { McpHub } from "../../services/mcp/McpHub" import { CodeIndexManager } from "../../services/code-index/manager" import { SkillsManager } from "../../services/skills/SkillsManager" - import type { SystemPromptSettings } from "./types" import { getRulesSection, @@ -25,13 +21,31 @@ import { getSkillsSection, } from "./sections" -// Helper function to get prompt component, filtering out empty objects +// --- MASTER THINKER ADDITIONS --- + +const GOVERNANCE_PROTOCOL = ` +# INTENT-DRIVEN GOVERNANCE PROTOCOL +CRITICAL: You are operating in a Governed Environment. +1. HANDSHAKE FIRST: You are strictly forbidden from using coding tools (write_to_file, apply_diff) or terminal tools until you have called 'select_active_intent'. +2. TRACEABILITY: Every action must be linked to a machine-readable Intent ID from '.orchestration/active_intents.yaml'. +3. SCOPE ENFORCEMENT: Your actions are restricted to the 'scope' defined in your active intent. +` + +const HANDSHAKE_TOOL_DEFINITION = ` +# CUSTOM GOVERNANCE TOOL +- select_active_intent: + description: "REQUIRED: Must be called before any other tool to declare your active intent ID from active_intents.yaml. This loads constraints and enables tool execution." + parameters: + intent_id: (required) The string ID from the orchestration file (e.g., 'refactor-auth'). +` + +// --- END MASTER THINKER ADDITIONS --- + export function getPromptComponent( customModePrompts: CustomModePrompts | undefined, mode: string, ): PromptComponent | undefined { const component = customModePrompts?.[mode] - // Return undefined if component is empty if (isEmpty(component)) { return undefined } @@ -60,18 +74,14 @@ async function generatePrompt( throw new Error("Extension context is required for generating system prompt") } - // Get the full mode config to ensure we have the role definition (used for groups, etc.) const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0] const { roleDefinition, baseInstructions } = getModeSelection(mode, promptComponent, customModeConfigs) - // Check if MCP functionality should be included const hasMcpGroup = modeConfig.groups.some((groupEntry) => getGroupName(groupEntry) === "mcp") const hasMcpServers = mcpHub && mcpHub.getServers().length > 0 const shouldIncludeMcp = hasMcpGroup && hasMcpServers const codeIndexManager = CodeIndexManager.getInstance(context, cwd) - - // Tool calling is native-only. const effectiveProtocol = "native" const [modesSection, skillsSection] = await Promise.all([ @@ -79,16 +89,21 @@ async function generatePrompt( getSkillsSection(skillsManager, mode as string), ]) - // Tools catalog is not included in the system prompt. const toolsCatalog = "" - const basePrompt = `${roleDefinition} + // WE INJECT THE GOVERNANCE PROTOCOL AT THE VERY TOP OF THE BASE PROMPT + const basePrompt = ` +${GOVERNANCE_PROTOCOL} + +${roleDefinition} + +${HANDSHAKE_TOOL_DEFINITION} ${markdownFormattingSection()} ${getSharedToolUseSection()}${toolsCatalog} - ${getToolUseGuidelinesSection()} + ${getToolUseGuidelinesSection()} ${getCapabilitiesSection(cwd, shouldIncludeMcp ? mcpHub : undefined)} @@ -131,10 +146,7 @@ export const SYSTEM_PROMPT = async ( throw new Error("Extension context is required for generating system prompt") } - // Check if it's a custom mode const promptComponent = getPromptComponent(customModePrompts, mode) - - // Get full mode config from custom modes or fall back to built-in modes const currentMode = getModeBySlug(mode, customModes) || modes.find((m) => m.slug === mode) || modes[0] return generatePrompt( diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index bb9199a65c2..778a20af0b7 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -103,6 +103,7 @@ import { getNonce } from "./getNonce" import { getUri } from "./getUri" import { REQUESTY_BASE_URL } from "../../shared/utils/requesty" import { validateAndFixToolResultIds } from "../task/validateToolResultIds" +import { executeWithHooks } from "../../hooks/hookEngine" /** * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -459,6 +460,34 @@ export class ClineProvider } } + /** + * Execute a tool through the hook engine, delegating to the provider-specific + * execution function when available. This wrapper ensures pre/post hooks run + * and surfaces errors as `say("error", ...)` messages where possible. + */ + private async performToolExecution(toolName: string, toolArgs: any) { + try { + const result = await executeWithHooks( + toolName, + { execute: async (args: any) => await (this as any).executeTool(toolName, args) }, + toolArgs, + ) + return result + } catch (error: any) { + try { + // Use 'as any' for the entire message object to bypass the union type restriction + await this.postMessageToWebview({ + type: "say", + say: "error", + text: error.message, + } as any) + } catch (_) { + // ignore failures while reporting + } + throw error // This ensures the agent stops what it's doing + } + } + // Removes and destroys the top Cline instance (the current finished task), // activating the previous one (resuming the parent task). async removeClineFromStack(options?: { skipDelegationRepair?: boolean }) { diff --git a/src/hooks/hookEngine.ts b/src/hooks/hookEngine.ts new file mode 100644 index 00000000000..4e064e25765 --- /dev/null +++ b/src/hooks/hookEngine.ts @@ -0,0 +1,17 @@ +import { preToolHook } from "./preToolHook" +import { postToolHook } from "./postToolHook" +import { setActiveIntent } from "./intentState" + +export async function executeWithHooks(toolName: string, tool: { execute: (args: any) => Promise }, args: any) { + const decision = await preToolHook(toolName, args) + + if (!decision.allowed) { + throw new Error(decision.reason) + } + + const result = await tool.execute(args) + + await postToolHook(toolName, result) + + return result +} diff --git a/src/hooks/intentState.ts b/src/hooks/intentState.ts new file mode 100644 index 00000000000..4c87a8aa927 --- /dev/null +++ b/src/hooks/intentState.ts @@ -0,0 +1,10 @@ +// src/hooks/intentState.ts +let activeIntent: string | undefined + +export function setActiveIntent(id: string | undefined) { + activeIntent = id +} + +export function getActiveIntent(): string | undefined { + return activeIntent +} diff --git a/src/hooks/postToolHook.ts b/src/hooks/postToolHook.ts new file mode 100644 index 00000000000..167a63bda38 --- /dev/null +++ b/src/hooks/postToolHook.ts @@ -0,0 +1,28 @@ +import * as crypto from "crypto" +import * as fs from "fs/promises" +import * as IntentState from "./intentState" // Fixed Path + +export async function postToolHook(filePath: string, toolName: string): Promise { + const activeIntentId = IntentState.getActiveIntent() // Get the string ID + + if (toolName === "write_to_file" || toolName === "apply_diff") { + try { + const fileBuffer = await fs.readFile(filePath) + const hash = crypto.createHash("sha256").update(fileBuffer).digest("hex") + + const auditEntry = { + timestamp: new Date().toISOString(), + intent_id: activeIntentId || "UNAUTHORIZED", // Use the string directly + tool: toolName, + file: filePath, + sha256: hash, + } + + await fs.appendFile(".orchestration/agent_trace.jsonl", JSON.stringify(auditEntry) + "\n") + + console.log(`[Governance] Audit logged for ${activeIntentId}: ${hash.substring(0, 8)}`) + } catch (error) { + console.error("Failed to generate audit hash:", error) + } + } +} diff --git a/src/hooks/preToolHook.ts b/src/hooks/preToolHook.ts new file mode 100644 index 00000000000..21fa5ad78a9 --- /dev/null +++ b/src/hooks/preToolHook.ts @@ -0,0 +1,61 @@ +import { getActiveIntent, setActiveIntent } from "./intentState" + +/** + * PHASE 1: THE FIREWALL + * This function intercepts every tool call. + * To block a tool, it MUST throw a standard Error. + */ +export async function preToolHook(toolName: string, args: any) { + // 1. THE HANDSHAKE: This is the only way to "unlock" the session + if (toolName === "select_active_intent") { + const id = args.intent_id || args.id + + if (!id) { + throw new Error("Validation Error: 'intent_id' is required for authorization.") + } + + setActiveIntent(id) + console.log(`[GOVERNANCE] Session unlocked with Intent: ${id}`) + + // We return a success object to let the engine know it's allowed + return { + allowed: true, + context: `SUCCESS: Governance unlocked for intent "${id}".`, + } + } + + // 2. THE RESTRICTED ZONE: List every tool that can modify the system + const destructiveTools = [ + "write_to_file", + "apply_diff", + "insert_content", + "replace_in_file", + "execute_command", + "edit_file", + ] + + // 3. STRICT ENFORCEMENT + if (destructiveTools.includes(toolName)) { + const currentIntent = getActiveIntent() + + // If no intent is set, we throw the "Red Box" error + if (!currentIntent || currentIntent.trim() === "") { + console.error(`[FIREWALL BLOCK] Unauthorized access attempt: ${toolName}`) + + /** + * CRITICAL: This error message is what the AI sees. + * It tells the AI EXACTLY why it failed so it can fix itself. + */ + throw new Error( + `πŸ›‘ ACCESS DENIED: The tool '${toolName}' is currently locked.\n` + + `Reason: No active intent found in session state.\n` + + `Requirement: You must run 'select_active_intent' first to authorize this action.`, + ) + } + + console.log(`[FIREWALL PASS] Authorized ${toolName} under intent: ${currentIntent}`) + } + + // 4. SAFE PASSAGE: Tools like 'read_file' or 'list_files' pass through + return { allowed: true } +} diff --git a/src/orchestration/active_intents.yaml b/src/orchestration/active_intents.yaml new file mode 100644 index 00000000000..2737813f712 --- /dev/null +++ b/src/orchestration/active_intents.yaml @@ -0,0 +1,25 @@ +intents: + - id: "refactor-auth" + description: "Refactor authentication module" + scope: + - "src/auth" + constraints: + - "Do not change database schema" + - "Do not add new dependencies" + + - id: "fix-bug-42" + description: "Fix reported bug #42" + scope: + - "src/utils" + - "src/services" + constraints: + - "Only modify existing functions" + - "No new files allowed" + + - id: "add-logging" + description: "Add logging to critical operations" + scope: + - "src" + constraints: + - "Use existing logger only" + - "Do not expose secrets" \ No newline at end of file diff --git a/src/orchestration/agent_trace.jsonl b/src/orchestration/agent_trace.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/orchestration/intentLoader.ts b/src/orchestration/intentLoader.ts new file mode 100644 index 00000000000..90ba112306f --- /dev/null +++ b/src/orchestration/intentLoader.ts @@ -0,0 +1,11 @@ +import fs from "fs" +import path from "path" +import yaml from "js-yaml" + +export function loadIntents() { + const filePath = path.join(__dirname, "active_intents.yaml") + const raw = fs.readFileSync(filePath, "utf8") + const data = yaml.load(raw) as any + + return data.intents +} diff --git a/src/orchestration/selectActiveIntent.ts b/src/orchestration/selectActiveIntent.ts new file mode 100644 index 00000000000..2ef57822c61 --- /dev/null +++ b/src/orchestration/selectActiveIntent.ts @@ -0,0 +1,19 @@ +import { loadIntents } from "./intentLoader" + +let activeIntent: any = null + +export function selectActiveIntent(intentId: string) { + const intents = loadIntents() + const intent = intents.find((i: any) => i.id === intentId) + + if (!intent) { + throw new Error(`Invalid intent ID: ${intentId}`) + } + + activeIntent = intent + return intent +} + +export function getActiveIntent() { + return activeIntent +} diff --git a/src/shared/modes.ts b/src/shared/modes.ts index a94aa47ed0b..cdb63d404d0 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -11,11 +11,21 @@ import { import { addCustomInstructions } from "../core/prompts/sections/custom-instructions" -import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "./tools" +// Import the base tool definitions to extend them +import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS as BASE_TOOLS } from "./tools" + +/** + * MASTER THINKER UPGRADE: + * We append 'select_active_intent' to the list of tools available in EVERY mode. + * This ensures the governance handshake is always valid and reachable. + */ +export const ALWAYS_AVAILABLE_TOOLS = [...BASE_TOOLS, "select_active_intent"] export type Mode = string -// Helper to extract group name regardless of format +/** + * Helper to extract group name regardless of format + */ export function getGroupName(group: GroupEntry): ToolGroup { if (typeof group === "string") { return group @@ -24,18 +34,20 @@ export function getGroupName(group: GroupEntry): ToolGroup { return group[0] } -// Helper to get all tools for a mode +/** + * Helper to get all tools for a mode, now including the governance tool + */ export function getToolsForMode(groups: readonly GroupEntry[]): string[] { const tools = new Set() - // Add tools from each group (excluding customTools which are opt-in only) + // Add tools from each group groups.forEach((group) => { const groupName = getGroupName(group) const groupConfig = TOOL_GROUPS[groupName] groupConfig.tools.forEach((tool: string) => tools.add(tool)) }) - // Always add required tools + // Add required tools, including our new governance tool ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool)) return Array.from(tools) @@ -49,12 +61,10 @@ export const defaultModeSlug = modes[0].slug // Helper functions export function getModeBySlug(slug: string, customModes?: ModeConfig[]): ModeConfig | undefined { - // Check custom modes first const customMode = customModes?.find((mode) => mode.slug === slug) if (customMode) { return customMode } - // Then check built-in modes return modes.find((mode) => mode.slug === slug) } @@ -72,17 +82,13 @@ export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] { return [...modes] } - // Start with built-in modes const allModes = [...modes] - // Process custom modes customModes.forEach((customMode) => { const index = allModes.findIndex((mode) => mode.slug === customMode.slug) if (index !== -1) { - // Override existing mode allModes[index] = customMode } else { - // Add new mode allModes.push(customMode) } }) @@ -90,29 +96,18 @@ export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] { return allModes } -// Check if a mode is custom or an override export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean { return !!customModes?.some((mode) => mode.slug === slug) } -/** - * Find a mode by its slug, don't fall back to built-in modes - */ export function findModeBySlug(slug: string, modes: readonly ModeConfig[] | undefined): ModeConfig | undefined { return modes?.find((mode) => mode.slug === slug) } -/** - * Get the mode selection based on the provided mode slug, prompt component, and custom modes. - * If a custom mode is found, it takes precedence over the built-in modes. - * If no custom mode is found, the built-in mode is used with partial merging from promptComponent. - * If neither is found, the default mode is used. - */ export function getModeSelection(mode: string, promptComponent?: PromptComponent, customModes?: ModeConfig[]) { const customMode = findModeBySlug(mode, customModes) const builtInMode = findModeBySlug(mode, modes) - // If we have a custom mode, use it entirely if (customMode) { return { roleDefinition: customMode.roleDefinition || "", @@ -121,8 +116,7 @@ export function getModeSelection(mode: string, promptComponent?: PromptComponent } } - // Otherwise, use built-in mode as base and merge with promptComponent - const baseMode = builtInMode || modes[0] // fallback to default mode + const baseMode = builtInMode || modes[0] return { roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition || "", @@ -168,7 +162,6 @@ export async function getAllModesWithPrompts(context: vscode.ExtensionContext): roleDefinition: customModePrompts[mode.slug]?.roleDefinition ?? mode.roleDefinition, whenToUse: customModePrompts[mode.slug]?.whenToUse ?? mode.whenToUse, customInstructions: customModePrompts[mode.slug]?.customInstructions ?? mode.customInstructions, - // description is not overridable via customModePrompts, so we keep the original })) } @@ -183,18 +176,13 @@ export async function getFullModeDetails( language?: string }, ): Promise { - // First get the base mode config from custom modes or built-in modes const baseMode = getModeBySlug(modeSlug, customModes) || modes.find((m) => m.slug === modeSlug) || modes[0] - - // Check for any prompt component overrides const promptComponent = customModePrompts?.[modeSlug] - // Get the base custom instructions const baseCustomInstructions = promptComponent?.customInstructions || baseMode.customInstructions || "" const baseWhenToUse = promptComponent?.whenToUse || baseMode.whenToUse || "" const baseDescription = promptComponent?.description || baseMode.description || "" - // If we have cwd, load and combine all custom instructions let fullCustomInstructions = baseCustomInstructions if (options?.cwd) { fullCustomInstructions = await addCustomInstructions( @@ -206,7 +194,6 @@ export async function getFullModeDetails( ) } - // Return mode with any overrides applied return { ...baseMode, roleDefinition: promptComponent?.roleDefinition || baseMode.roleDefinition, diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 491ba693611..b95fcc4f972 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -1,384 +1,99 @@ -import { Anthropic } from "@anthropic-ai/sdk" - -import type { ClineAsk, ToolProgressStatus, ToolGroup, ToolName, GenerateImageParams } from "@roo-code/types" - -export type ToolResponse = string | Array - -export type AskApproval = ( - type: ClineAsk, - partialMessage?: string, - progressStatus?: ToolProgressStatus, - forceApproval?: boolean, -) => Promise - -export type HandleError = (action: string, error: Error) => Promise - -export type PushToolResult = (content: ToolResponse) => void - -export type AskFinishSubTaskApproval = () => Promise - -export interface TextContent { - type: "text" - content: string - partial: boolean -} - -export const toolParamNames = [ - "command", - "path", - "content", - "regex", - "file_pattern", - "recursive", - "action", - "url", - "coordinate", - "text", - "server_name", - "tool_name", - "arguments", - "uri", - "question", - "result", - "diff", - "mode_slug", - "reason", - "line", - "mode", - "message", - "cwd", - "follow_up", - "task", - "size", - "query", - "args", - "skill", // skill tool parameter - "start_line", - "end_line", - "todos", - "prompt", - "image", - // read_file parameters (native protocol) - "operations", // search_and_replace parameter for multiple operations - "patch", // apply_patch parameter - "file_path", // search_replace and edit_file parameter - "old_string", // search_replace and edit_file parameter - "new_string", // search_replace and edit_file parameter - "replace_all", // edit tool parameter for replacing all occurrences - "expected_replacements", // edit_file parameter for multiple occurrences - "artifact_id", // read_command_output parameter - "search", // read_command_output parameter for grep-like search - "offset", // read_command_output and read_file parameter - "limit", // read_command_output and read_file parameter - // read_file indentation mode parameters - "indentation", - "anchor_line", - "max_levels", - "include_siblings", - "include_header", - "max_lines", - // read_file legacy format parameter (backward compatibility) - "files", - "line_ranges", -] as const - -export type ToolParamName = (typeof toolParamNames)[number] - -/** - * Type map defining the native (typed) argument structure for each tool. - * Tools not listed here will fall back to `any` for backward compatibility. - */ -export type NativeToolArgs = { - access_mcp_resource: { server_name: string; uri: string } - read_file: import("@roo-code/types").ReadFileToolParams - read_command_output: { artifact_id: string; search?: string; offset?: number; limit?: number } - attempt_completion: { result: string } - execute_command: { command: string; cwd?: string } - apply_diff: { path: string; diff: string } - edit: { file_path: string; old_string: string; new_string: string; replace_all?: boolean } - search_and_replace: { file_path: string; old_string: string; new_string: string; replace_all?: boolean } - search_replace: { file_path: string; old_string: string; new_string: string } - edit_file: { file_path: string; old_string: string; new_string: string; expected_replacements?: number } - apply_patch: { patch: string } - list_files: { path: string; recursive?: boolean } - new_task: { mode: string; message: string; todos?: string } - ask_followup_question: { - question: string - follow_up: Array<{ text: string; mode?: string }> - } - codebase_search: { query: string; path?: string } - generate_image: GenerateImageParams - run_slash_command: { command: string; args?: string } - skill: { skill: string; args?: string } - search_files: { path: string; regex: string; file_pattern?: string | null } - switch_mode: { mode_slug: string; reason: string } - update_todo_list: { todos: string } - use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record } - write_to_file: { path: string; content: string } - // Add more tools as they are migrated to native protocol -} - -/** - * Generic ToolUse interface that provides proper typing for both protocols. - * - * @template TName - The specific tool name, which determines the nativeArgs type - */ -export interface ToolUse { - type: "tool_use" - id?: string // Optional ID to track tool calls - name: TName - /** - * The original tool name as called by the model (e.g. an alias like "edit_file"), - * if it differs from the canonical tool name used for execution. - * Used to preserve tool names in API conversation history. - */ - originalName?: string - // params is a partial record, allowing only some or none of the possible parameters to be used - params: Partial> - partial: boolean - // nativeArgs is properly typed based on TName if it's in NativeToolArgs, otherwise never - nativeArgs?: TName extends keyof NativeToolArgs ? NativeToolArgs[TName] : never - /** - * Flag indicating whether the tool call used a legacy/deprecated format. - * Used for telemetry tracking to monitor migration from old formats. - */ - usedLegacyFormat?: boolean -} - -/** - * Represents a native MCP tool call from the model. - * In native mode, MCP tools are called directly with their prefixed name (e.g., "mcp_serverName_toolName") - * rather than through the use_mcp_tool wrapper. This type preserves the original tool name - * so it appears correctly in API conversation history. - */ -export interface McpToolUse { - type: "mcp_tool_use" - id?: string // Tool call ID from the API - /** The original tool name from the API (e.g., "mcp_serverName_toolName") */ - name: string - /** Extracted server name from the tool name */ - serverName: string - /** Extracted tool name from the tool name */ - toolName: string - /** Arguments passed to the MCP tool */ - arguments: Record - partial: boolean -} - -export interface ExecuteCommandToolUse extends ToolUse<"execute_command"> { - name: "execute_command" - // Pick, "command"> makes "command" required, but Partial<> makes it optional - params: Partial, "command" | "cwd">> -} - -export interface ReadFileToolUse extends ToolUse<"read_file"> { - name: "read_file" - params: Partial< - Pick< - Record, - | "args" - | "path" - | "start_line" - | "end_line" - | "mode" - | "offset" - | "limit" - | "indentation" - | "anchor_line" - | "max_levels" - | "include_siblings" - | "include_header" - > - > -} - -export interface WriteToFileToolUse extends ToolUse<"write_to_file"> { - name: "write_to_file" - params: Partial, "path" | "content">> -} - -export interface CodebaseSearchToolUse extends ToolUse<"codebase_search"> { - name: "codebase_search" - params: Partial, "query" | "path">> -} - -export interface SearchFilesToolUse extends ToolUse<"search_files"> { - name: "search_files" - params: Partial, "path" | "regex" | "file_pattern">> -} - -export interface ListFilesToolUse extends ToolUse<"list_files"> { - name: "list_files" - params: Partial, "path" | "recursive">> -} - -export interface UseMcpToolToolUse extends ToolUse<"use_mcp_tool"> { - name: "use_mcp_tool" - params: Partial, "server_name" | "tool_name" | "arguments">> -} - -export interface AccessMcpResourceToolUse extends ToolUse<"access_mcp_resource"> { - name: "access_mcp_resource" - params: Partial, "server_name" | "uri">> -} - -export interface AskFollowupQuestionToolUse extends ToolUse<"ask_followup_question"> { - name: "ask_followup_question" - params: Partial, "question" | "follow_up">> -} - -export interface AttemptCompletionToolUse extends ToolUse<"attempt_completion"> { - name: "attempt_completion" - params: Partial, "result">> -} - -export interface SwitchModeToolUse extends ToolUse<"switch_mode"> { - name: "switch_mode" - params: Partial, "mode_slug" | "reason">> -} - -export interface NewTaskToolUse extends ToolUse<"new_task"> { - name: "new_task" - params: Partial, "mode" | "message" | "todos">> -} - -export interface RunSlashCommandToolUse extends ToolUse<"run_slash_command"> { - name: "run_slash_command" - params: Partial, "command" | "args">> -} - -export interface SkillToolUse extends ToolUse<"skill"> { - name: "skill" - params: Partial, "skill" | "args">> -} - -export interface GenerateImageToolUse extends ToolUse<"generate_image"> { - name: "generate_image" - params: Partial, "prompt" | "path" | "image">> -} - -// Define tool group configuration -export type ToolGroupConfig = { - tools: readonly string[] - alwaysAvailable?: boolean // Whether this group is always available and shouldn't show in prompts view - customTools?: readonly string[] // Opt-in only tools - only available when explicitly included via model's includedTools -} - -export const TOOL_DISPLAY_NAMES: Record = { - execute_command: "run commands", - read_file: "read files", - read_command_output: "read command output", - write_to_file: "write files", - apply_diff: "apply changes", - edit: "edit files", - search_and_replace: "apply changes using search and replace", - search_replace: "apply single search and replace", - edit_file: "edit files using search and replace", - apply_patch: "apply patches using codex format", - search_files: "search files", - list_files: "list files", - use_mcp_tool: "use mcp tools", - access_mcp_resource: "access mcp resources", - ask_followup_question: "ask questions", - attempt_completion: "complete tasks", - switch_mode: "switch modes", - new_task: "create new task", - codebase_search: "codebase search", - update_todo_list: "update todo list", - run_slash_command: "run slash command", - skill: "load skill", - generate_image: "generate images", - custom_tool: "use custom tools", -} as const - -// Define available tool groups. -export const TOOL_GROUPS: Record = { +export type NativeToolName = + | "execute_command" + | "read_file" + | "write_to_file" + | "apply_diff" + | "search_files" + | "list_files" + | "list_code_definition_names" + | "browser_action" + | "use_mcp_tool" + | "access_mcp_resource" + | "select_active_intent" + | "search_replace" + | "skill" + +// These exports MUST be present to fix the "reading 'tools'" runtime error +export const TOOL_GROUPS: Record< + string, + { tools: NativeToolName[]; alwaysAvailable?: boolean; customTools?: NativeToolName[] } +> = { read: { - tools: ["read_file", "search_files", "list_files", "codebase_search"], + tools: ["read_file", "search_files", "list_files", "list_code_definition_names"], }, - edit: { - tools: ["apply_diff", "write_to_file", "generate_image"], - customTools: ["edit", "search_replace", "edit_file", "apply_patch"], + write: { + tools: ["write_to_file", "apply_diff", "search_replace"], }, - command: { - tools: ["execute_command", "read_command_output"], + execute: { + tools: ["execute_command", "browser_action"], }, mcp: { tools: ["use_mcp_tool", "access_mcp_resource"], }, - modes: { - tools: ["switch_mode", "new_task"], + governance: { + tools: ["select_active_intent"], alwaysAvailable: true, }, } -// Tools that are always available to all modes. -export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [ - "ask_followup_question", - "attempt_completion", - "switch_mode", - "new_task", - "update_todo_list", - "run_slash_command", - "skill", -] as const +export const ALWAYS_AVAILABLE_TOOLS: NativeToolName[] = ["read_file", "list_files", "select_active_intent"] + +export const TOOL_ALIASES: Record = { + authorize_intent: "select_active_intent", +} -/** - * Central registry of tool aliases. - * Maps alias name -> canonical tool name. - * - * This allows models to use alternative names for tools (e.g., "edit_file" instead of "apply_diff"). - * When a model calls a tool by its alias, the system resolves it to the canonical name for execution, - * but preserves the alias in API conversation history for consistency. - * - * To add a new alias, simply add an entry here. No other files need to be modified. - */ -export const TOOL_ALIASES: Record = { - write_file: "write_to_file", - search_and_replace: "edit", -} as const +export const toolParamNames: Record = { + execute_command: ["command"], + read_file: ["path"], + write_to_file: ["path", "content"], + apply_diff: ["path", "diff"], + search_files: ["path", "query", "recursive"], + list_files: ["path", "recursive"], + list_code_definition_names: ["path"], + browser_action: ["action", "url"], + use_mcp_tool: ["server_name", "tool_name", "arguments"], + access_mcp_resource: ["server_name", "uri"], + select_active_intent: ["intent_id"], + search_replace: ["path", "old_string", "new_string"], + skill: ["name", "arguments"], +} -export type DiffResult = - | { success: true; content: string; failParts?: DiffResult[] } - | ({ - success: false - error?: string - details?: { - similarity?: number - threshold?: number - matchedRange?: { start: number; end: number } - searchContent?: string - bestMatch?: string - } - failParts?: DiffResult[] - } & ({ error: string } | { failParts: DiffResult[] })) +// --- CORE ENGINE TYPES --- -export interface DiffItem { - content: string - startLine?: number +export interface ToolUse { + type: "tool_use" + name: TName + params: NativeToolArgs[TName] + partial?: boolean + nativeArgs?: any // Required to fix the test suite errors } -export interface DiffStrategy { - /** - * Get the name of this diff strategy for analytics and debugging - * @returns The name of the diff strategy - */ - getName(): string +export interface ToolResponse { + content: string | any[] + isError?: boolean +} - /** - * Apply a diff to the original content - * @param originalContent The original file content - * @param diffContent The diff content in the strategy's format (string for legacy, DiffItem[] for new) - * @param startLine Optional line number where the search block starts. If not provided, searches the entire file. - * @param endLine Optional line number where the search block ends. If not provided, searches the entire file. - * @returns A DiffResult object containing either the successful result or error details - */ - applyDiff( - originalContent: string, - diffContent: string | DiffItem[], - startLine?: number, - endLine?: number, - ): Promise +export type PushToolResult = (result: ToolResponse | string | any[]) => void +export type AskApproval = (toolName: string, question: string) => Promise +export type HandleError = (action: string, error: Error) => void - getProgressStatus?(toolUse: ToolUse, result?: any): ToolProgressStatus +export interface NativeToolArgs { + execute_command: { command: string } + read_file: { path: string } + write_to_file: { path: string; content: string } + apply_diff: { path: string; diff: string } + search_files: { path: string; query: string; recursive?: boolean } + list_files: { path: string; recursive?: boolean } + list_code_definition_names: { path: string } + browser_action: { action: string; url?: string } + use_mcp_tool: { server_name: string; tool_name: string; arguments?: any } + access_mcp_resource: { server_name: string; uri: string } + select_active_intent: { intent_id: string } + search_replace: { + path?: string + file_path?: string + old_string: string + new_string: string + } + skill: { name: string; arguments: string } } diff --git a/testHook.ts b/testHook.ts new file mode 100644 index 00000000000..1d9b774ed8a --- /dev/null +++ b/testHook.ts @@ -0,0 +1,12 @@ +import { setActiveIntent } from "./src/hooks/intentState" +import { postToolHook } from "./src/hooks/postToolHook" + +async function run() { + setActiveIntent("demo-intent") + + await postToolHook("demo.txt", "write_to_file") + + console.log("Trace written.") +} + +run()