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:
Leuel Asfaw 2026-02-21 14:07:50 +03:00 committed by leuel-a
commit 62db67f949
7 changed files with 246 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,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)
}
}
}

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

11
src/utils/hash.ts Normal file
View 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)
}