setup initial code semantic tracking ledger as post hooks

This commit is contained in:
leuel-a 2026-02-21 13:57:42 +03:00
parent ec41ccc661
commit 8864a4102e
7 changed files with 183 additions and 27 deletions

View file

@ -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
}
}

View file

@ -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<MiddlewareResult> {
// No pre-execution logic needed for tracing
return { allow: true }
}
async afterExecute(result: any, task: Task, toolName: string): Promise<MiddlewareResult> {
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<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)
}
}
}

View file

@ -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 }
}
}

View file

@ -4,6 +4,7 @@ export interface MiddlewareResult {
allow: boolean
error?: string
modifiedParams?: any
modifiedResult?: any
}
export interface ToolMiddleware {

View file

@ -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,
},
},

View file

@ -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"> {

5
src/utils/hash.ts Normal file
View file

@ -0,0 +1,5 @@
import * as crypto from "crypto"
export function generateContentHash(content: string): string {
return crypto.createHash("sha256").update(content, "utf8").digest("hex")
}