mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-19 00:01:19 +00:00
fix: change implementation for the select_active_intent tool
This commit is contained in:
parent
e0d0019192
commit
8897933060
14 changed files with 273 additions and 48 deletions
|
|
@ -681,6 +681,23 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: requiresIntent check should be based on tool definition, not hardcoded list
|
||||
const requiresIntent: ToolName[] = [
|
||||
"apply_diff",
|
||||
"write_to_file",
|
||||
"edit_file",
|
||||
"apply_patch",
|
||||
"execute_command",
|
||||
"search_replace",
|
||||
"edit",
|
||||
]
|
||||
|
||||
if (requiresIntent.includes(block.name as ToolName) && !cline.getHasSelectedIntent()) {
|
||||
const errorMsg = "You must call select_active_intent before using modification tools."
|
||||
pushToolResult(formatResponse.toolError(errorMsg))
|
||||
break
|
||||
}
|
||||
|
||||
switch (block.name) {
|
||||
case "list_active_intents":
|
||||
await listActiveIntentsTool.handle(cline, block as ToolUse<"list_active_intents">, {
|
||||
|
|
|
|||
85
src/core/intents/IntentLoader.ts
Normal file
85
src/core/intents/IntentLoader.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { logger } from "../../utils/logging"
|
||||
import type { Intent, ActiveIntentsFile } from "./types"
|
||||
|
||||
export class IntentLoader {
|
||||
private intents: Map<string, Intent> = new Map()
|
||||
private cwd: string
|
||||
private readonly log = logger.child({ component: "IntentLoader" })
|
||||
private lastLoadTime = 0
|
||||
private readonly CACHE_TTL_MS = 5000 // 5 seconds
|
||||
|
||||
constructor(cwd: string) {
|
||||
this.cwd = cwd
|
||||
}
|
||||
|
||||
async ensureLoaded(force = false): Promise<void> {
|
||||
const now = Date.now()
|
||||
if (!force && this.intents.size > 0 && now - this.lastLoadTime < this.CACHE_TTL_MS) {
|
||||
// Cache is still valid
|
||||
return
|
||||
}
|
||||
|
||||
await this.loadIntents()
|
||||
this.lastLoadTime = now
|
||||
}
|
||||
|
||||
private async loadIntents(): Promise<void> {
|
||||
const intentsPath = path.join(this.cwd, ".orchestration", "active_intents.json")
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(intentsPath, "utf-8")
|
||||
const parsed = this.parseJsonSafely(content, intentsPath)
|
||||
|
||||
if (!parsed?.active_intents || !Array.isArray(parsed.active_intents)) {
|
||||
this.log.warn(`No active_intents found in ${intentsPath}`)
|
||||
this.intents.clear()
|
||||
return
|
||||
}
|
||||
|
||||
this.intents.clear()
|
||||
|
||||
for (const intent of parsed.active_intents) {
|
||||
if (intent?.id && typeof intent.id === "string") {
|
||||
this.intents.set(intent.id, intent)
|
||||
}
|
||||
}
|
||||
|
||||
this.log.info(`Loaded ${this.intents.size} intents from ${intentsPath}`)
|
||||
} catch (error: any) {
|
||||
if (error?.code !== "ENOENT") {
|
||||
this.log.error(`Failed to load intents from ${intentsPath}`, error)
|
||||
}
|
||||
this.intents.clear()
|
||||
}
|
||||
}
|
||||
|
||||
getIntent(id: string): Intent | undefined {
|
||||
return this.intents.get(id)
|
||||
}
|
||||
|
||||
getAllIntents(): Intent[] {
|
||||
return Array.from(this.intents.values())
|
||||
}
|
||||
|
||||
hasIntent(id: string): boolean {
|
||||
return this.intents.has(id)
|
||||
}
|
||||
|
||||
private parseJsonSafely(content: string, filePath: string): ActiveIntentsFile {
|
||||
try {
|
||||
const cleaned = this.stripBom(content)
|
||||
const parsed = JSON.parse(cleaned)
|
||||
return (parsed ?? { active_intents: [] }) as ActiveIntentsFile
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
this.log.error(`Failed to parse JSON from ${filePath}: ${msg}`)
|
||||
return { active_intents: [] }
|
||||
}
|
||||
}
|
||||
|
||||
private stripBom(s: string): string {
|
||||
return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s
|
||||
}
|
||||
}
|
||||
16
src/core/intents/types.ts
Normal file
16
src/core/intents/types.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
enum INTENT_STATUS {
|
||||
IN_PROGRESS,
|
||||
}
|
||||
|
||||
export interface ActiveIntentsFile {
|
||||
active_intents: Intent[]
|
||||
}
|
||||
|
||||
export interface Intent {
|
||||
id: string
|
||||
name: string
|
||||
status: INTENT_STATUS
|
||||
owned_scopes: string[]
|
||||
constraints: string[]
|
||||
acceptance_criteria: string[]
|
||||
}
|
||||
|
|
@ -199,6 +199,13 @@ Otherwise, if you have not completed the task and do not need additional informa
|
|||
const prettyPatchLines = lines.slice(4)
|
||||
return prettyPatchLines.join("\n")
|
||||
},
|
||||
|
||||
invalidIntentId: (intentId: string) =>
|
||||
JSON.stringify({
|
||||
status: "error",
|
||||
type: "invalid_intent",
|
||||
intent_id: intentId,
|
||||
}),
|
||||
}
|
||||
|
||||
// to avoid circular dependency
|
||||
|
|
|
|||
|
|
@ -8,3 +8,5 @@ export { getCapabilitiesSection } from "./capabilities"
|
|||
export { getModesSection } from "./modes"
|
||||
export { markdownFormattingSection } from "./markdown-formatting"
|
||||
export { getSkillsSection } from "./skills"
|
||||
export { intentIndexSection } from "./intent-index"
|
||||
export { intentProtocolSection } from "./intent-protocol"
|
||||
|
|
|
|||
16
src/core/prompts/sections/intent-index.ts
Normal file
16
src/core/prompts/sections/intent-index.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Intent } from "../../intents/types"
|
||||
|
||||
export function intentIndexSection(intents: Intent[]) {
|
||||
const rows = (intents ?? []).slice(0, 30).map((i) => {
|
||||
const scope = (i.owned_scopes ?? []).slice(0, 4).join(", ")
|
||||
return `- ${i.id}: ${i.name} [${i.status}] scope: ${scope}${(i.owned_scopes?.length ?? 0) > 4 ? ", ..." : ""}`
|
||||
})
|
||||
|
||||
const extra =
|
||||
(intents?.length ?? 0) > 30 ? `\n(Showing 30 of ${intents.length}. Use list_active_intents for full list.)` : ""
|
||||
|
||||
return `
|
||||
[ACTIVE INTENT INDEX]
|
||||
${rows.length ? rows.join("\n") : "- (none found)"}${extra}
|
||||
`.trim()
|
||||
}
|
||||
14
src/core/prompts/sections/intent-protocol.ts
Normal file
14
src/core/prompts/sections/intent-protocol.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export function intentProtocolSection(): string {
|
||||
return `
|
||||
===
|
||||
|
||||
INTENT-DRIVEN PROTOCOL
|
||||
|
||||
1. You MUST call the tool "select_active_intent" before making any code edits or running destructive commands.
|
||||
2. You may call read-only tools before selecting an intent, but you must select an intent before:
|
||||
- apply_diff / write_to_file / edit_file / apply_patch
|
||||
- execute_command that changes the repo (git commit, installs, deletions, etc.)
|
||||
3. If you are unsure which intent applies, request the list of intents from the user or consult the intent index below.
|
||||
4. After selecting an intent, you must keep all actions within owned_scope and obey constraints.
|
||||
`.trim()
|
||||
}
|
||||
|
|
@ -23,6 +23,8 @@ import {
|
|||
addCustomInstructions,
|
||||
markdownFormattingSection,
|
||||
getSkillsSection,
|
||||
intentProtocolSection,
|
||||
intentIndexSection,
|
||||
} from "./sections"
|
||||
|
||||
// Helper function to get prompt component, filtering out empty objects
|
||||
|
|
@ -92,6 +94,8 @@ ${getSharedToolUseSection()}${toolsCatalog}
|
|||
|
||||
${getCapabilitiesSection(cwd, shouldIncludeMcp ? mcpHub : undefined)}
|
||||
|
||||
${intentProtocolSection()}
|
||||
|
||||
${modesSection}
|
||||
${skillsSection ? `\n${skillsSection}` : ""}
|
||||
${getRulesSection(cwd, settings)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import type OpenAI from "openai"
|
||||
|
||||
const SELECT_ACTIVE_INTENT_DESCRIPTION = ` `
|
||||
const INTENT_ID_DESCRIPTION = ``
|
||||
const SELECT_ACTIVE_INTENT_DESCRIPTION = `
|
||||
Select and activate an intent from active_intents.json.\n
|
||||
This must be called before making any changes to understand what changes are intended.\n
|
||||
`
|
||||
const INTENT_ID_DESCRIPTION = `The intent_id for the currently selected intent by the LLM and will be activated by LLM`
|
||||
|
||||
export default {
|
||||
type: "function",
|
||||
|
|
|
|||
|
|
@ -350,6 +350,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolResultBlockParam)[] = []
|
||||
userMessageContentReady = false
|
||||
|
||||
// Intent
|
||||
private selectedIntentId?: string
|
||||
private hasSelectedIntent = false
|
||||
|
||||
/**
|
||||
* Flag indicating whether the assistant message for the current streaming session
|
||||
* has been saved to API conversation history.
|
||||
|
|
@ -1263,6 +1267,19 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
return undefined
|
||||
}
|
||||
|
||||
public setSelectedIntent(intentId: string): void {
|
||||
this.selectedIntentId = intentId
|
||||
this.hasSelectedIntent = true
|
||||
}
|
||||
|
||||
public getHasSelectedIntent(): boolean {
|
||||
return this.hasSelectedIntent
|
||||
}
|
||||
|
||||
public getSelectedIntentId(): string | undefined {
|
||||
return this.selectedIntentId
|
||||
}
|
||||
|
||||
// Note that `partial` has three valid states true (partial message),
|
||||
// false (completion of partial message), undefined (individual complete
|
||||
// message).
|
||||
|
|
@ -4008,6 +4025,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
Task.lastGlobalApiRequestTime = performance.now()
|
||||
|
||||
const systemPrompt = await this.getSystemPrompt()
|
||||
|
||||
const { contextTokens } = this.getTokenUsage()
|
||||
|
||||
if (contextTokens) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { Task } from "../task/Task"
|
||||
import { BaseTool, ToolCallbacks } from "./BaseTool"
|
||||
|
||||
export class ListActiveIntent extends BaseTool<"list_active_intents"> {
|
||||
export class ListActiveIntents extends BaseTool<"list_active_intents"> {
|
||||
readonly name = "list_active_intents" as const
|
||||
|
||||
override execute(_params: any, _task: Task, _callbacks: ToolCallbacks): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
export const listActiveIntentsTool = new ListActiveIntents()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,82 @@
|
|||
import { Task } from "../task/Task"
|
||||
import { BaseTool, ToolCallbacks } from "./BaseTool"
|
||||
import { Intent } from "../intents/types"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
|
||||
interface SelectActiveIntentParams {
|
||||
intent_id: string
|
||||
}
|
||||
|
||||
export class SelectActiveIntent extends BaseTool<"select_active_intent"> {
|
||||
export class SelectActiveIntentTool extends BaseTool<"select_active_intent"> {
|
||||
readonly name = "select_active_intent" as const
|
||||
|
||||
override execute(_params: SelectActiveIntentParams, _task: Task, _callbacks: ToolCallbacks): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
async execute(params: SelectActiveIntentParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
|
||||
const { intent_id } = params
|
||||
const { handleError, pushToolResult } = callbacks
|
||||
|
||||
try {
|
||||
if (!intent_id) {
|
||||
task.consecutiveMistakeCount++
|
||||
task.recordToolError("select_active_intent")
|
||||
task.didToolFailInCurrentTurn = true
|
||||
pushToolResult(await task.sayAndCreateMissingParamError("select_active_intent", "intent_id"))
|
||||
return
|
||||
}
|
||||
|
||||
const provider = task.providerRef.deref()
|
||||
if (!provider) {
|
||||
return
|
||||
}
|
||||
|
||||
const intentLoader = provider.getIntentLoader()
|
||||
await intentLoader.ensureLoaded()
|
||||
|
||||
const intent = intentLoader.getIntent(intent_id)
|
||||
if (!intent) {
|
||||
task.setSelectedIntent(intent_id)
|
||||
pushToolResult(formatResponse.invalidIntentId(intent_id))
|
||||
return
|
||||
}
|
||||
|
||||
pushToolResult(this.formatIntentContextXml(intent))
|
||||
} catch (error) {
|
||||
handleError("selecting intents", error)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Formats the selected intent as an <intent_context> XML block for prompt injection.
|
||||
* Keep this deterministic and safe (escape XML).
|
||||
*/
|
||||
private formatIntentContextXml(intent: Intent): string {
|
||||
const escapeXml = (text: string): string =>
|
||||
String(text)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
|
||||
const ownedScopes = intent.owned_scopes ?? []
|
||||
|
||||
const renderList = (containerTag: string, itemTag: string, items: string[]): string => {
|
||||
const inner = (items ?? []).map((x) => ` <${itemTag}>${escapeXml(x)}</${itemTag}>`).join("\n")
|
||||
|
||||
return items && items.length
|
||||
? `<${containerTag}>\n${inner}\n </${containerTag}>`
|
||||
: `<${containerTag}></${containerTag}>`
|
||||
}
|
||||
|
||||
return [
|
||||
`<intent_context>`,
|
||||
` <id>${escapeXml(intent.id)}</id>`,
|
||||
` <name>${escapeXml(intent.name)}</name>`,
|
||||
` <status>${escapeXml(intent.status as unknown as string)}</status>`,
|
||||
` ${renderList("owned_scope", "path", ownedScopes)}`,
|
||||
` ${renderList("constraints", "constraint", intent.constraints ?? [])}`,
|
||||
` ${renderList("acceptance_criteria", "criteria", intent.acceptance_criteria ?? [])}`,
|
||||
`</intent_context>`,
|
||||
].join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
export const selectActiveIntentTool = new SelectActiveIntent()
|
||||
export const selectActiveIntentTool = new SelectActiveIntentTool()
|
||||
|
|
|
|||
|
|
@ -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 { IntentLoader } from "../intents/IntentLoader"
|
||||
|
||||
/**
|
||||
* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
|
|
@ -170,6 +171,11 @@ export class ClineProvider
|
|||
public readonly providerSettingsManager: ProviderSettingsManager
|
||||
public readonly customModesManager: CustomModesManager
|
||||
|
||||
/**
|
||||
* Intent Loader
|
||||
*/
|
||||
private intentLoader?: IntentLoader
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
private readonly outputChannel: vscode.OutputChannel,
|
||||
|
|
@ -420,6 +426,14 @@ export class ClineProvider
|
|||
}
|
||||
}
|
||||
|
||||
public getIntentLoader(): IntentLoader {
|
||||
if (!this.intentLoader) {
|
||||
const cwd = this.currentWorkspacePath || process.cwd()
|
||||
this.intentLoader = new IntentLoader(cwd)
|
||||
}
|
||||
return this.intentLoader
|
||||
}
|
||||
|
||||
// Adds a new Task instance to clineStack, marking the start of a new task.
|
||||
// The instance is pushed to the top of the stack (LIFO order).
|
||||
// When the task is completed, the top instance is removed, reactivating the
|
||||
|
|
@ -659,6 +673,7 @@ export class ClineProvider
|
|||
}
|
||||
}
|
||||
|
||||
this.intentLoader = undefined
|
||||
this._workspaceTracker?.dispose()
|
||||
this._workspaceTracker = undefined
|
||||
await this.mcpHub?.unregisterClient()
|
||||
|
|
|
|||
|
|
@ -1,41 +1 @@
|
|||
export class HookEngine {
|
||||
private preHooks: PreHook[] = []
|
||||
private postHooks: PostHook[] = []
|
||||
|
||||
registerPre(hook: PreHook) {
|
||||
this.preHooks.push(hook)
|
||||
}
|
||||
|
||||
registerPost(hook: PostHook) {
|
||||
this.postHooks.push(hook)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a tool execution with pre/post hooks.
|
||||
* `exec` is your existing tool runner: (toolName, args) => result
|
||||
*/
|
||||
async runTool(
|
||||
call: ToolCall,
|
||||
exec: (call: ToolCall) => Promise<ToolResult>,
|
||||
ctx: HookContext,
|
||||
): Promise<ToolResult> {
|
||||
for (const hook of this.preHooks) {
|
||||
const decision = await hook(call, ctx)
|
||||
if (decision.action === "short_circuit") {
|
||||
// Even short-circuited results go through post hooks (optional, but useful)
|
||||
for (const post of this.postHooks) {
|
||||
await post(call, decision.result, ctx)
|
||||
}
|
||||
return decision.result
|
||||
}
|
||||
}
|
||||
|
||||
const result = await exec(call)
|
||||
|
||||
for (const post of this.postHooks) {
|
||||
await post(call, result, ctx)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
export class HookEngine {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue