From 8864a4102e99caac02ceb795cd4f56735b9d35af Mon Sep 17 00:00:00 2001 From: leuel-a Date: Sat, 21 Feb 2026 13:57:42 +0300 Subject: [PATCH] setup initial code semantic tracking ledger as post hooks --- .../presentAssistantMessage.ts | 68 ++++++----- src/core/middlewares/AgentTraceMiddleware.ts | 110 ++++++++++++++++++ src/core/middlewares/MiddlewareChain.ts | 13 +++ src/core/middlewares/ToolMiddleware.ts | 1 + .../tools/native-tools/write_to_file.ts | 11 +- src/core/tools/WriteToFileTool.ts | 2 + src/utils/hash.ts | 5 + 7 files changed, 183 insertions(+), 27 deletions(-) create mode 100644 src/core/middlewares/AgentTraceMiddleware.ts create mode 100644 src/utils/hash.ts diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index ab2b404536..306b820dd1 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -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 } } diff --git a/src/core/middlewares/AgentTraceMiddleware.ts b/src/core/middlewares/AgentTraceMiddleware.ts new file mode 100644 index 0000000000..afa2282438 --- /dev/null +++ b/src/core/middlewares/AgentTraceMiddleware.ts @@ -0,0 +1,110 @@ +// src/core/middleware/AgentTraceMiddleware.ts +import * as fs from "fs/promises" +import * as path from "path" +import { generateContentHash } from "../../utils/hash" +import { Task } from "../task/Task" +import type { ToolMiddleware, MiddlewareResult } from "./ToolMiddleware" + +interface AgentTraceEntry { + timestamp: string + intent_id: string + mutation_class: "AST_REFACTOR" | "INTENT_EVOLUTION" + file_path: string + content_hash: string + related: string[] // REQ-IDs + ranges: { + start: number + end: number + content_hash: string + }[] +} + +interface AgentTraceEntry { + timestamp: string + intent_id: string + mutation_class: "AST_REFACTOR" | "INTENT_EVOLUTION" + file_path: string + content_hash: string + related: string[] + ranges: { + start: number + end: number + content_hash: string + }[] +} + +export class AgentTraceMiddleware implements ToolMiddleware { + name = "agentTrace" + + async beforeExecute(params: any, task: Task, toolName: string): Promise { + // No pre-execution logic needed for tracing + return { allow: true } + } + + async afterExecute(result: any, task: Task, toolName: string): Promise { + if (toolName !== "write_to_file") { + return { allow: true, modifiedResult: result } + } + + try { + // Get the last write_file tool use from the conversation + const lastToolUse = this.findLastToolUse(task, "write_to_file") + if (!lastToolUse) { + return { allow: true, modifiedResult: result } + } + + const { intent_id, mutation_class, path: filePath } = lastToolUse.params + const contentHash = generateContentHash(lastToolUse.params.content) + + const traceEntry: AgentTraceEntry = { + timestamp: new Date().toISOString(), + intent_id, + mutation_class, + file_path: filePath, + content_hash: contentHash, + related: [intent_id], // Inject REQ-ID into related array + ranges: [ + { + start: 0, + end: lastToolUse.params.content.length, + content_hash: contentHash, // Inject content_hash into ranges + }, + ], + } + + await this.appendToTraceFile(traceEntry, task.cwd) + return { allow: true, modifiedResult: result } + } catch (error) { + console.error("Agent trace middleware error:", error) + // Don't fail the operation, just log the error + return { allow: true, modifiedResult: result } + } + } + + 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 { + 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) + } + } +} diff --git a/src/core/middlewares/MiddlewareChain.ts b/src/core/middlewares/MiddlewareChain.ts index c3c85a88c3..8e38dd1ab8 100644 --- a/src/core/middlewares/MiddlewareChain.ts +++ b/src/core/middlewares/MiddlewareChain.ts @@ -22,4 +22,17 @@ export class MiddlewareChain { } return { allow: true } } + + async executeAfter(result: any, task: Task, toolName: ToolName): Promise { + 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 } + } } diff --git a/src/core/middlewares/ToolMiddleware.ts b/src/core/middlewares/ToolMiddleware.ts index 02063e1626..9a470bddf7 100644 --- a/src/core/middlewares/ToolMiddleware.ts +++ b/src/core/middlewares/ToolMiddleware.ts @@ -4,6 +4,7 @@ export interface MiddlewareResult { allow: boolean error?: string modifiedParams?: any + modifiedResult?: any } export interface ToolMiddleware { diff --git a/src/core/prompts/tools/native-tools/write_to_file.ts b/src/core/prompts/tools/native-tools/write_to_file.ts index b9e9b313a2..2b64577768 100644 --- a/src/core/prompts/tools/native-tools/write_to_file.ts +++ b/src/core/prompts/tools/native-tools/write_to_file.ts @@ -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, }, }, diff --git a/src/core/tools/WriteToFileTool.ts b/src/core/tools/WriteToFileTool.ts index c8455ef3d9..4535df37c0 100644 --- a/src/core/tools/WriteToFileTool.ts +++ b/src/core/tools/WriteToFileTool.ts @@ -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"> { diff --git a/src/utils/hash.ts b/src/utils/hash.ts new file mode 100644 index 0000000000..9f6ad20cd5 --- /dev/null +++ b/src/utils/hash.ts @@ -0,0 +1,5 @@ +import * as crypto from "crypto" + +export function generateContentHash(content: string): string { + return crypto.createHash("sha256").update(content, "utf8").digest("hex") +}