mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-12 23:01:21 +00:00
Merge pull request #3 from leuel-a/feat/add-semantic-tracking-ledger
setup initial code semantic tracking ledger as post hooks
This commit is contained in:
commit
62db67f949
7 changed files with 246 additions and 27 deletions
|
|
@ -45,6 +45,7 @@ import { sanitizeToolUseId } from "../../utils/tool-id"
|
|||
import { MiddlewareChain } from "../middlewares/MiddlewareChain"
|
||||
import { IntentValidationMiddleware } from "../middlewares/IntentValidationMiddleware"
|
||||
import { ScopeEnforcementMiddleware } from "../middlewares/ScopeEnforcementMiddleware"
|
||||
import { AgentTraceMiddleware } from "../middlewares/AgentTraceMiddleware"
|
||||
|
||||
/**
|
||||
* Processes and presents assistant message content to the user interface.
|
||||
|
|
@ -687,6 +688,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
const middlewareChain = new MiddlewareChain()
|
||||
middlewareChain.add(new IntentValidationMiddleware())
|
||||
middlewareChain.add(new ScopeEnforcementMiddleware())
|
||||
middlewareChain.add(new AgentTraceMiddleware())
|
||||
|
||||
const middlewareResult = await middlewareChain.executeBefore(block.params, cline, block.name)
|
||||
if (!middlewareResult.allow) {
|
||||
|
|
@ -697,19 +699,25 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
break
|
||||
}
|
||||
|
||||
let capturedToolResult: any = null
|
||||
const capturePushToolResult = (content: ToolResponse) => {
|
||||
capturedToolResult = content
|
||||
return pushToolResult(content)
|
||||
}
|
||||
|
||||
switch (block.name) {
|
||||
case "list_active_intents":
|
||||
await listActiveIntentsTool.handle(cline, block as ToolUse<"list_active_intents">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "select_active_intent":
|
||||
await selectActiveIntentTool.handle(cline, block as ToolUse<"select_active_intent">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "write_to_file":
|
||||
|
|
@ -717,14 +725,14 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "update_todo_list":
|
||||
await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "apply_diff":
|
||||
|
|
@ -732,7 +740,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "edit":
|
||||
|
|
@ -741,7 +749,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await editTool.handle(cline, block as ToolUse<"edit">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "search_replace":
|
||||
|
|
@ -749,7 +757,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await searchReplaceTool.handle(cline, block as ToolUse<"search_replace">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "edit_file":
|
||||
|
|
@ -757,7 +765,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await editFileTool.handle(cline, block as ToolUse<"edit_file">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "apply_patch":
|
||||
|
|
@ -765,7 +773,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "read_file":
|
||||
|
|
@ -773,70 +781,70 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await readFileTool.handle(cline, block as ToolUse<"read_file">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "list_files":
|
||||
await listFilesTool.handle(cline, block as ToolUse<"list_files">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "codebase_search":
|
||||
await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "search_files":
|
||||
await searchFilesTool.handle(cline, block as ToolUse<"search_files">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "execute_command":
|
||||
await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "read_command_output":
|
||||
await readCommandOutputTool.handle(cline, block as ToolUse<"read_command_output">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "use_mcp_tool":
|
||||
await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "access_mcp_resource":
|
||||
await accessMcpResourceTool.handle(cline, block as ToolUse<"access_mcp_resource">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "ask_followup_question":
|
||||
await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "switch_mode":
|
||||
await switchModeTool.handle(cline, block as ToolUse<"switch_mode">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "new_task":
|
||||
|
|
@ -844,7 +852,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await newTaskTool.handle(cline, block as ToolUse<"new_task">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
toolCallId: block.id,
|
||||
})
|
||||
break
|
||||
|
|
@ -852,7 +860,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
const completionCallbacks: AttemptCompletionCallbacks = {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
askFinishSubTaskApproval,
|
||||
toolDescription,
|
||||
}
|
||||
|
|
@ -867,14 +875,14 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await runSlashCommandTool.handle(cline, block as ToolUse<"run_slash_command">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "skill":
|
||||
await skillTool.handle(cline, block as ToolUse<"skill">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
case "generate_image":
|
||||
|
|
@ -882,7 +890,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
await generateImageTool.handle(cline, block as ToolUse<"generate_image">, {
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
pushToolResult: capturedToolResult,
|
||||
})
|
||||
break
|
||||
default: {
|
||||
|
|
@ -910,7 +918,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
console.error(message)
|
||||
cline.consecutiveMistakeCount++
|
||||
await cline.say("error", message)
|
||||
pushToolResult(formatResponse.toolError(message))
|
||||
capturedToolResult(formatResponse.toolError(message))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -924,7 +932,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
`${customTool.name}.execute(): ${JSON.stringify(customToolArgs)} -> ${JSON.stringify(result)}`,
|
||||
)
|
||||
|
||||
pushToolResult(result)
|
||||
capturedToolResult(result)
|
||||
cline.consecutiveMistakeCount = 0
|
||||
} catch (executionError: any) {
|
||||
cline.consecutiveMistakeCount++
|
||||
|
|
@ -953,6 +961,14 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
}
|
||||
}
|
||||
|
||||
const postResult = await middlewareChain.executeAfter(capturedToolResult, cline, block.name)
|
||||
if (postResult.modifiedResult !== undefined) {
|
||||
const lastResult = cline.userMessageContent[cline.userMessageContent.length - 1]
|
||||
if (lastResult?.type === "tool_result") {
|
||||
lastResult.content = postResult.modifiedResult
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
167
src/core/middlewares/AgentTraceMiddleware.ts
Normal file
167
src/core/middlewares/AgentTraceMiddleware.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as crypto from "crypto"
|
||||
import { Task } from "../task/Task"
|
||||
import { generateContentHash } from "../../utils/hash"
|
||||
import type { ToolMiddleware, MiddlewareResult } from "./ToolMiddleware"
|
||||
|
||||
interface AgentTraceEntry {
|
||||
id: string
|
||||
timestamp: string
|
||||
vcs: {
|
||||
revision_id: string
|
||||
}
|
||||
files: Array<{
|
||||
relative_path: string
|
||||
conversations: Array<{
|
||||
url: string
|
||||
contributor: {
|
||||
entity_type: "AI"
|
||||
model_identifier: string
|
||||
}
|
||||
ranges: Array<{
|
||||
start_line: number
|
||||
end_line: number
|
||||
content_hash: string
|
||||
}>
|
||||
related: Array<{
|
||||
type: "specification"
|
||||
value: string
|
||||
}>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
export class AgentTraceMiddleware implements ToolMiddleware {
|
||||
name = "agentTrace"
|
||||
|
||||
async beforeExecute(_params: any, _task: Task, _toolName: string): Promise<MiddlewareResult> {
|
||||
return { allow: true }
|
||||
}
|
||||
|
||||
async afterExecute(result: any, task: Task, toolName: string): Promise<MiddlewareResult> {
|
||||
if (toolName !== "write_to_file") {
|
||||
return { allow: true, modifiedResult: result }
|
||||
}
|
||||
|
||||
try {
|
||||
const lastToolUse = this.findLastToolUse(task, "write_to_file")
|
||||
if (!lastToolUse) {
|
||||
return { allow: true, modifiedResult: result }
|
||||
}
|
||||
|
||||
const { intent_id, mutation_class, path: filePath, content } = lastToolUse.params
|
||||
const contentHash = generateContentHash(content)
|
||||
|
||||
const traceEntry: AgentTraceEntry = {
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
vcs: {
|
||||
revision_id: await this.getGitSha(task.cwd),
|
||||
},
|
||||
files: [
|
||||
{
|
||||
relative_path: filePath,
|
||||
conversations: [
|
||||
{
|
||||
url: task.taskId, // session_log_id
|
||||
contributor: {
|
||||
entity_type: "AI",
|
||||
model_identifier: await this.getModelIdentifier(task),
|
||||
},
|
||||
ranges: [
|
||||
{
|
||||
start_line: 1,
|
||||
end_line: content.split("\n").length,
|
||||
content_hash: `sha256:${contentHash}`,
|
||||
},
|
||||
],
|
||||
related: [
|
||||
{
|
||||
type: "specification",
|
||||
value: intent_id, // REQ-ID injection
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await this.appendToTraceFile(traceEntry, task.cwd)
|
||||
return { allow: true, modifiedResult: result }
|
||||
} catch (error) {
|
||||
console.error("Agent trace middleware error:", error)
|
||||
return { allow: true, modifiedResult: result }
|
||||
}
|
||||
}
|
||||
|
||||
private async getGitSha(cwd: string): Promise<string> {
|
||||
try {
|
||||
const { execSync } = require("child_process")
|
||||
const gitSha = execSync("git rev-parse HEAD", {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
}).trim()
|
||||
return gitSha || "unknown"
|
||||
} catch {
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private async getModelIdentifier(task: Task): Promise<string> {
|
||||
try {
|
||||
const provider = task.providerRef.deref()
|
||||
if (!provider) return "unknown"
|
||||
|
||||
const state = await provider.getState()
|
||||
const apiConfiguration = state?.apiConfiguration
|
||||
|
||||
if (apiConfiguration) {
|
||||
switch (apiConfiguration.apiProvider) {
|
||||
case "anthropic":
|
||||
return apiConfiguration.apiModelId || "unknown"
|
||||
case "openrouter":
|
||||
return apiConfiguration.openRouterModelId || "unknown"
|
||||
case "openai":
|
||||
return apiConfiguration.openAiModelId || "unknown"
|
||||
case "lmstudio":
|
||||
return apiConfiguration.lmStudioModelId || "unknown"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
} catch {
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private findLastToolUse(task: Task, toolName: string): any {
|
||||
for (let i = task.apiConversationHistory.length - 1; i >= 0; i--) {
|
||||
const message = task.apiConversationHistory[i]
|
||||
if (message.role === "assistant" && Array.isArray(message?.content)) {
|
||||
const toolUse = message.content?.find(
|
||||
(block: any) => block.type === "tool_use" && block.name === toolName,
|
||||
)
|
||||
if (toolUse) {
|
||||
return toolUse
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private async appendToTraceFile(entry: AgentTraceEntry, cwd: string): Promise<void> {
|
||||
const tracePath = path.join(cwd, ".orchestration", "agent_trace.jsonl")
|
||||
|
||||
try {
|
||||
await fs.mkdir(path.dirname(tracePath), { recursive: true })
|
||||
const line = JSON.stringify(entry) + "\n"
|
||||
await fs.appendFile(tracePath, line, "utf8")
|
||||
} catch (error) {
|
||||
console.error("Failed to write to agent trace:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,4 +22,17 @@ export class MiddlewareChain {
|
|||
}
|
||||
return { allow: true }
|
||||
}
|
||||
|
||||
async executeAfter(result: any, task: Task, toolName: ToolName): Promise<MiddlewareResult> {
|
||||
let finalResult = result
|
||||
|
||||
for (const middleware of this.middlewares) {
|
||||
const postResult = await middleware.afterExecute?.(finalResult, task, toolName)
|
||||
if (postResult?.modifiedResult !== undefined) {
|
||||
finalResult = postResult.modifiedResult
|
||||
}
|
||||
}
|
||||
|
||||
return { allow: true, modifiedResult: finalResult }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export interface MiddlewareResult {
|
|||
allow: boolean
|
||||
error?: string
|
||||
modifiedParams?: any
|
||||
modifiedResult?: any
|
||||
}
|
||||
|
||||
export interface ToolMiddleware {
|
||||
|
|
|
|||
|
|
@ -32,8 +32,17 @@ export default {
|
|||
type: "string",
|
||||
description: CONTENT_PARAMETER_DESCRIPTION,
|
||||
},
|
||||
intent_id: {
|
||||
type: "string",
|
||||
description: "The intent ID this write operation belongs to (e.g., REQ-001)",
|
||||
},
|
||||
mutation_class: {
|
||||
type: "string",
|
||||
enum: ["AST_REFACTOR", "INTENT_EVOLUTION"],
|
||||
description: "Classification: AST_REFACTOR for syntax changes, INTENT_EVOLUTION for new features",
|
||||
},
|
||||
},
|
||||
required: ["path", "content"],
|
||||
required: ["path", "content", "intent_id", "mutation_class"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import { BaseTool, ToolCallbacks } from "./BaseTool"
|
|||
interface WriteToFileParams {
|
||||
path: string
|
||||
content: string
|
||||
intent_id: string
|
||||
mutation_class: "AST_REFACTOR" | "INTENT_EVOLUTION"
|
||||
}
|
||||
|
||||
export class WriteToFileTool extends BaseTool<"write_to_file"> {
|
||||
|
|
|
|||
11
src/utils/hash.ts
Normal file
11
src/utils/hash.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import * as crypto from "crypto"
|
||||
|
||||
export function generateContentHash(content: string): string {
|
||||
return crypto.createHash("sha256").update(content, "utf8").digest("hex")
|
||||
}
|
||||
|
||||
export function generateBlockHash(content: string, startLine: number, endLine: number): string {
|
||||
const lines = content.split("\n")
|
||||
const blockContent = lines.slice(startLine - 1, endLine).join("\n")
|
||||
return generateContentHash(blockContent)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue