mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-06 02:47:56 +00:00
Record tool usages in the Cline object, and persist them in the db for evals (#2729)
This commit is contained in:
parent
b5a77e34a4
commit
87af3b3424
58 changed files with 977 additions and 222 deletions
|
|
@ -275,7 +275,12 @@ const runExercise = async ({ run, task, server }: { run: Run; task: Task; server
|
|||
})
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskCompleted || eventName === RooCodeEventName.TaskAborted) {
|
||||
if (eventName === RooCodeEventName.TaskCompleted && taskMetricsId) {
|
||||
const toolUsage = payload[2]
|
||||
await updateTaskMetrics(taskMetricsId, { toolUsage })
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskAborted || eventName === RooCodeEventName.TaskCompleted) {
|
||||
taskFinishedAt = Date.now()
|
||||
await updateTask(task.id, { finishedAt: new Date() })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Ellipsis, Rocket } from "lucide-react"
|
|||
import type { Run, TaskMetrics } from "@evals/db"
|
||||
|
||||
import { deleteRun } from "@/lib/server/runs"
|
||||
import { formatCurrency, formatDuration, formatTokens } from "@/lib"
|
||||
import { formatCurrency, formatDuration, formatTokens, formatToolUsageSuccessRate } from "@/lib/formatters"
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
|
|
@ -59,7 +59,8 @@ export function Home({ runs }: { runs: (Run & { taskMetrics: TaskMetrics | null
|
|||
<TableHead>Passed</TableHead>
|
||||
<TableHead>Failed</TableHead>
|
||||
<TableHead>% Correct</TableHead>
|
||||
<TableHead className="text-center">Tokens In / Out</TableHead>
|
||||
<TableHead>Tokens In / Out</TableHead>
|
||||
<TableHead>Diff Edits</TableHead>
|
||||
<TableHead>Cost</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead />
|
||||
|
|
@ -79,12 +80,21 @@ export function Home({ runs }: { runs: (Run & { taskMetrics: TaskMetrics | null
|
|||
</TableCell>
|
||||
<TableCell>
|
||||
{taskMetrics && (
|
||||
<div className="flex items-center justify-evenly">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div>{formatTokens(taskMetrics.tokensIn)}</div>/
|
||||
<div>{formatTokens(taskMetrics.tokensOut)}</div>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{taskMetrics?.toolUsage?.apply_diff && (
|
||||
<div className="flex flex-row items-center gap-1.5">
|
||||
<div>{taskMetrics.toolUsage.apply_diff.attempts}</div>
|
||||
<div>/</div>
|
||||
<div>{formatToolUsageSuccessRate(taskMetrics.toolUsage.apply_diff)}</div>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{taskMetrics && formatCurrency(taskMetrics.cost)}</TableCell>
|
||||
<TableCell>{taskMetrics && formatDuration(taskMetrics.duration)}</TableCell>
|
||||
<TableCell>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { LoaderCircle } from "lucide-react"
|
|||
|
||||
import * as db from "@evals/db"
|
||||
|
||||
import { formatCurrency, formatDuration, formatTokens } from "@/lib"
|
||||
import { formatCurrency, formatDuration, formatTokens } from "@/lib/formatters"
|
||||
import { useRunStatus } from "@/hooks/use-run-status"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
})
|
||||
|
||||
export const formatCurrency = (amount: number) => formatter.format(amount)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
export const formatDuration = (durationMs: number) => {
|
||||
const seconds = Math.floor(durationMs / 1000)
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
|
||||
const parts = []
|
||||
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours}h`)
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes}m`)
|
||||
}
|
||||
|
||||
if (remainingSeconds > 0 || parts.length === 0) {
|
||||
parts.push(`${remainingSeconds}s`)
|
||||
}
|
||||
|
||||
return parts.join(" ")
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
export const formatTokens = (tokens: number) => {
|
||||
if (tokens < 1000) {
|
||||
return tokens.toString()
|
||||
}
|
||||
|
||||
if (tokens < 1000000) {
|
||||
return `${(tokens / 1000).toFixed(1)}k`
|
||||
}
|
||||
|
||||
if (tokens < 1000000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`
|
||||
}
|
||||
|
||||
return `${(tokens / 1000000000).toFixed(1)}B`
|
||||
}
|
||||
48
evals/apps/web/src/lib/formatters.ts
Normal file
48
evals/apps/web/src/lib/formatters.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
})
|
||||
|
||||
export const formatCurrency = (amount: number) => formatter.format(amount)
|
||||
|
||||
export const formatDuration = (durationMs: number) => {
|
||||
const seconds = Math.floor(durationMs / 1000)
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
|
||||
const parts = []
|
||||
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours}h`)
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes}m`)
|
||||
}
|
||||
|
||||
if (remainingSeconds > 0 || parts.length === 0) {
|
||||
parts.push(`${remainingSeconds}s`)
|
||||
}
|
||||
|
||||
return parts.join(" ")
|
||||
}
|
||||
|
||||
export const formatTokens = (tokens: number) => {
|
||||
if (tokens < 1000) {
|
||||
return tokens.toString()
|
||||
}
|
||||
|
||||
if (tokens < 1000000) {
|
||||
return `${(tokens / 1000).toFixed(1)}k`
|
||||
}
|
||||
|
||||
if (tokens < 1000000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`
|
||||
}
|
||||
|
||||
return `${(tokens / 1000000000).toFixed(1)}B`
|
||||
}
|
||||
|
||||
export const formatToolUsageSuccessRate = (usage: { attempts: number; failures: number }) =>
|
||||
usage.attempts === 0 ? '0%' : `${(((usage.attempts - usage.failures) / usage.attempts) * 100).toFixed(1)}%`
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export { formatCurrency } from "./format-currency"
|
||||
export { formatDuration } from "./format-duration"
|
||||
export { formatTokens } from "./format-tokens"
|
||||
1
evals/packages/db/.gitignore
vendored
Normal file
1
evals/packages/db/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
test.db
|
||||
1
evals/packages/db/drizzle/0003_sweet_chimera.sql
Normal file
1
evals/packages/db/drizzle/0003_sweet_chimera.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `taskMetrics` ADD `toolUsage` text;
|
||||
296
evals/packages/db/drizzle/meta/0003_snapshot.json
Normal file
296
evals/packages/db/drizzle/meta/0003_snapshot.json
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "61d48d20-f662-445d-9962-cf9cb165cbe7",
|
||||
"prevId": "f49d9b0b-fda9-467a-9adb-c941d6cbf7ce",
|
||||
"tables": {
|
||||
"runs": {
|
||||
"name": "runs",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"taskMetricsId": {
|
||||
"name": "taskMetricsId",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"type": "blob",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"pid": {
|
||||
"name": "pid",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"socketPath": {
|
||||
"name": "socketPath",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"concurrency": {
|
||||
"name": "concurrency",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 2
|
||||
},
|
||||
"passed": {
|
||||
"name": "passed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"failed": {
|
||||
"name": "failed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"runs_taskMetricsId_taskMetrics_id_fk": {
|
||||
"name": "runs_taskMetricsId_taskMetrics_id_fk",
|
||||
"tableFrom": "runs",
|
||||
"tableTo": "taskMetrics",
|
||||
"columnsFrom": ["taskMetricsId"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"taskMetrics": {
|
||||
"name": "taskMetrics",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"tokensIn": {
|
||||
"name": "tokensIn",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"tokensOut": {
|
||||
"name": "tokensOut",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"tokensContext": {
|
||||
"name": "tokensContext",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cacheWrites": {
|
||||
"name": "cacheWrites",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cacheReads": {
|
||||
"name": "cacheReads",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"toolUsage": {
|
||||
"name": "toolUsage",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"tasks": {
|
||||
"name": "tasks",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"runId": {
|
||||
"name": "runId",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"taskMetricsId": {
|
||||
"name": "taskMetricsId",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"exercise": {
|
||||
"name": "exercise",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"passed": {
|
||||
"name": "passed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"startedAt": {
|
||||
"name": "startedAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"finishedAt": {
|
||||
"name": "finishedAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tasks_language_exercise_idx": {
|
||||
"name": "tasks_language_exercise_idx",
|
||||
"columns": ["runId", "language", "exercise"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"tasks_runId_runs_id_fk": {
|
||||
"name": "tasks_runId_runs_id_fk",
|
||||
"tableFrom": "tasks",
|
||||
"tableTo": "runs",
|
||||
"columnsFrom": ["runId"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"tasks_taskMetricsId_taskMetrics_id_fk": {
|
||||
"name": "tasks_taskMetricsId_taskMetrics_id_fk",
|
||||
"tableFrom": "tasks",
|
||||
"tableTo": "taskMetrics",
|
||||
"columnsFrom": ["taskMetricsId"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,13 @@
|
|||
"when": 1743698195142,
|
||||
"tag": "0002_white_flatman",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1744950664129,
|
||||
"tag": "0003_sweet_chimera",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"scripts": {
|
||||
"lint": "eslint src/**/*.ts --max-warnings=0",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest --globals --run",
|
||||
"format": "prettier --write src",
|
||||
"drizzle-kit": "dotenvx run -f ../../.env -- tsx node_modules/drizzle-kit/bin.cjs",
|
||||
"db:generate": "pnpm drizzle-kit generate",
|
||||
|
|
@ -29,6 +30,8 @@
|
|||
"devDependencies": {
|
||||
"@evals/eslint-config": "workspace:^",
|
||||
"@evals/typescript-config": "workspace:^",
|
||||
"drizzle-kit": "^0.30.5"
|
||||
"drizzle-kit": "^0.30.5",
|
||||
"execa": "^9.5.2",
|
||||
"vitest": "^3.0.9"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
87
evals/packages/db/src/queries/__tests__/runs.test.ts
Normal file
87
evals/packages/db/src/queries/__tests__/runs.test.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { createRun, finishRun } from "../runs.js"
|
||||
import { createTask } from "../tasks.js"
|
||||
import { createTaskMetrics } from "../taskMetrics.js"
|
||||
|
||||
describe("finishRun", () => {
|
||||
it("aggregates task metrics, including tool usage", async () => {
|
||||
const run = await createRun({ model: "gpt-4.1-mini", socketPath: "/tmp/roo.sock" })
|
||||
|
||||
await createTask({
|
||||
runId: run.id,
|
||||
taskMetricsId: (
|
||||
await createTaskMetrics({
|
||||
duration: 45_000,
|
||||
tokensIn: 100_000,
|
||||
tokensOut: 2_000,
|
||||
tokensContext: 102_000,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
cost: 0.05,
|
||||
toolUsage: {
|
||||
read_file: {
|
||||
attempts: 3,
|
||||
failures: 0,
|
||||
},
|
||||
apply_diff: {
|
||||
attempts: 3,
|
||||
failures: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
).id,
|
||||
language: "go",
|
||||
exercise: "go/say",
|
||||
passed: true,
|
||||
startedAt: new Date(),
|
||||
finishedAt: new Date(),
|
||||
})
|
||||
|
||||
await createTask({
|
||||
runId: run.id,
|
||||
taskMetricsId: (
|
||||
await createTaskMetrics({
|
||||
duration: 30_000,
|
||||
tokensIn: 75_000,
|
||||
tokensOut: 1_000,
|
||||
tokensContext: 76_000,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
cost: 0.04,
|
||||
toolUsage: {
|
||||
read_file: {
|
||||
attempts: 3,
|
||||
failures: 0,
|
||||
},
|
||||
apply_diff: {
|
||||
attempts: 2,
|
||||
failures: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
).id,
|
||||
language: "go",
|
||||
exercise: "go/octal",
|
||||
passed: true,
|
||||
startedAt: new Date(),
|
||||
finishedAt: new Date(),
|
||||
})
|
||||
|
||||
const { taskMetrics } = await finishRun(run.id)
|
||||
|
||||
expect(taskMetrics).toEqual({
|
||||
id: expect.any(Number),
|
||||
tokensIn: 175000,
|
||||
tokensOut: 3000,
|
||||
tokensContext: 178000,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
cost: 0.09,
|
||||
duration: 75000,
|
||||
toolUsage: {
|
||||
read_file: { attempts: 6, failures: 0 },
|
||||
apply_diff: { attempts: 5, failures: 1 },
|
||||
},
|
||||
createdAt: expect.any(Date),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import { desc, eq, inArray, sql, sum } from "drizzle-orm"
|
||||
|
||||
import { ToolUsage } from "@evals/types"
|
||||
|
||||
import { RecordNotFoundError, RecordNotCreatedError } from "./errors.js"
|
||||
import type { InsertRun, UpdateRun } from "../schema.js"
|
||||
import { insertRunSchema, schema } from "../schema.js"
|
||||
import { db } from "../db.js"
|
||||
import { createTaskMetrics } from "./taskMetrics.js"
|
||||
import { getTasks } from "./tasks.js"
|
||||
|
||||
const table = schema.runs
|
||||
|
||||
|
|
@ -71,17 +74,30 @@ export const finishRun = async (runId: number) => {
|
|||
throw new RecordNotFoundError()
|
||||
}
|
||||
|
||||
const tasks = await getTasks(runId)
|
||||
|
||||
const toolUsage = tasks.reduce((acc, task) => {
|
||||
Object.entries(task.taskMetrics?.toolUsage || {}).forEach(([key, { attempts, failures }]) => {
|
||||
const tool = key as keyof ToolUsage
|
||||
acc[tool] ??= { attempts: 0, failures: 0 }
|
||||
acc[tool].attempts += attempts
|
||||
acc[tool].failures += failures
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {} as ToolUsage)
|
||||
|
||||
const { passed, failed, ...rest } = values
|
||||
const taskMetrics = await createTaskMetrics(rest)
|
||||
const taskMetrics = await createTaskMetrics({ ...rest, toolUsage })
|
||||
await updateRun(runId, { taskMetricsId: taskMetrics.id, passed, failed })
|
||||
|
||||
const run = await db.query.runs.findFirst({ where: eq(table.id, runId), with: { taskMetrics: true } })
|
||||
const run = await findRun(runId)
|
||||
|
||||
if (!run) {
|
||||
throw new RecordNotFoundError()
|
||||
}
|
||||
|
||||
return run
|
||||
return { ...run, taskMetrics }
|
||||
}
|
||||
|
||||
export const deleteRun = async (runId: number) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { sqliteTable, text, real, integer, blob, uniqueIndex } from "drizzle-orm
|
|||
import { relations } from "drizzle-orm"
|
||||
import { createInsertSchema } from "drizzle-zod"
|
||||
|
||||
import { RooCodeSettings, exerciseLanguages, rooCodeSettingsSchema } from "@evals/types"
|
||||
import { RooCodeSettings, ToolUsage, exerciseLanguages, rooCodeSettingsSchema, toolUsageSchema } from "@evals/types"
|
||||
|
||||
/**
|
||||
* runs
|
||||
|
|
@ -84,12 +84,15 @@ export const taskMetrics = sqliteTable("taskMetrics", {
|
|||
cacheReads: integer({ mode: "number" }).notNull(),
|
||||
cost: real().notNull(),
|
||||
duration: integer({ mode: "number" }).notNull(),
|
||||
toolUsage: text({ mode: "json" }).$type<ToolUsage>(),
|
||||
createdAt: integer({ mode: "timestamp" }).notNull(),
|
||||
})
|
||||
|
||||
export type TaskMetrics = typeof taskMetrics.$inferSelect
|
||||
|
||||
export const insertTaskMetricsSchema = createInsertSchema(taskMetrics).omit({ id: true, createdAt: true })
|
||||
export const insertTaskMetricsSchema = createInsertSchema(taskMetrics)
|
||||
.omit({ id: true, createdAt: true })
|
||||
.extend({ toolUsage: toolUsageSchema.optional() })
|
||||
|
||||
export type InsertTaskMetrics = Omit<typeof taskMetrics.$inferInsert, "id" | "createdAt">
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"extends": "@evals/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
7
evals/packages/db/vitest.config.ts
Normal file
7
evals/packages/db/vitest.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globalSetup: ["./vitest.setup.ts"],
|
||||
},
|
||||
})
|
||||
20
evals/packages/db/vitest.setup.ts
Normal file
20
evals/packages/db/vitest.setup.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
||||
import { execa } from "execa"
|
||||
|
||||
const TEST_DB_PATH = path.join(process.cwd(), "test.db")
|
||||
|
||||
export default async function () {
|
||||
const exists = await fs.stat(TEST_DB_PATH).catch(() => false)
|
||||
|
||||
if (exists) {
|
||||
await fs.unlink(TEST_DB_PATH)
|
||||
}
|
||||
|
||||
await execa({
|
||||
env: { BENCHMARKS_DB_PATH: `file:${TEST_DB_PATH}` },
|
||||
})`pnpm db:push`
|
||||
|
||||
process.env.BENCHMARKS_DB_PATH = `file:${TEST_DB_PATH}`
|
||||
}
|
||||
|
|
@ -59,6 +59,7 @@ export const rooCodeDefaults: RooCodeSettings = {
|
|||
search_and_replace: false,
|
||||
insert_content: false,
|
||||
powerSteering: false,
|
||||
append_to_file: false,
|
||||
},
|
||||
|
||||
language: "en",
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ export type CustomSupportPrompts = z.infer<typeof customSupportPromptsSchema>
|
|||
* ExperimentId
|
||||
*/
|
||||
|
||||
export const experimentIds = ["search_and_replace", "insert_content", "powerSteering"] as const
|
||||
export const experimentIds = ["search_and_replace", "insert_content", "powerSteering", "append_to_file"] as const
|
||||
|
||||
export const experimentIdsSchema = z.enum(experimentIds)
|
||||
|
||||
|
|
@ -285,6 +285,7 @@ const experimentsSchema = z.object({
|
|||
search_and_replace: z.boolean(),
|
||||
insert_content: z.boolean(),
|
||||
powerSteering: z.boolean(),
|
||||
append_to_file: z.boolean(),
|
||||
})
|
||||
|
||||
export type Experiments = z.infer<typeof experimentsSchema>
|
||||
|
|
@ -802,6 +803,49 @@ export const tokenUsageSchema = z.object({
|
|||
|
||||
export type TokenUsage = z.infer<typeof tokenUsageSchema>
|
||||
|
||||
/**
|
||||
* ToolName
|
||||
*/
|
||||
|
||||
export const toolNames = [
|
||||
"execute_command",
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"append_to_file",
|
||||
"apply_diff",
|
||||
"insert_content",
|
||||
"search_and_replace",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
"switch_mode",
|
||||
"new_task",
|
||||
"fetch_instructions",
|
||||
] as const
|
||||
|
||||
export const toolNamesSchema = z.enum(toolNames)
|
||||
|
||||
export type ToolName = z.infer<typeof toolNamesSchema>
|
||||
|
||||
/**
|
||||
* ToolUsage
|
||||
*/
|
||||
|
||||
export const toolUsageSchema = z.record(
|
||||
toolNamesSchema,
|
||||
z.object({
|
||||
attempts: z.number(),
|
||||
failures: z.number(),
|
||||
}),
|
||||
)
|
||||
|
||||
export type ToolUsage = z.infer<typeof toolUsageSchema>
|
||||
|
||||
/**
|
||||
* RooCodeEvent
|
||||
*/
|
||||
|
|
@ -837,7 +881,7 @@ export const rooCodeEventsSchema = z.object({
|
|||
[RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskAborted]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskSpawned]: z.tuple([z.string(), z.string()]),
|
||||
[RooCodeEventName.TaskCompleted]: z.tuple([z.string(), tokenUsageSchema]),
|
||||
[RooCodeEventName.TaskCompleted]: z.tuple([z.string(), tokenUsageSchema, toolUsageSchema]),
|
||||
[RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]),
|
||||
})
|
||||
|
||||
|
|
|
|||
6
evals/pnpm-lock.yaml
generated
6
evals/pnpm-lock.yaml
generated
|
|
@ -274,6 +274,12 @@ importers:
|
|||
drizzle-kit:
|
||||
specifier: ^0.30.5
|
||||
version: 0.30.5
|
||||
execa:
|
||||
specifier: ^9.5.2
|
||||
version: 9.5.2
|
||||
vitest:
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.3)
|
||||
|
||||
packages/ipc:
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@
|
|||
],
|
||||
"tasks": {
|
||||
"lint": {},
|
||||
"check-types": {
|
||||
"dependsOn": []
|
||||
},
|
||||
"check-types": {},
|
||||
"test": {},
|
||||
"format": {},
|
||||
"dev": {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { serializeError } from "serialize-error"
|
|||
import * as vscode from "vscode"
|
||||
|
||||
// schemas
|
||||
import { TokenUsage } from "../schemas"
|
||||
import { TokenUsage, ToolUsage, ToolName } from "../schemas"
|
||||
|
||||
// api
|
||||
import { ApiHandler, buildApiHandler } from "../api"
|
||||
|
|
@ -39,7 +39,7 @@ import { GlobalFileNames } from "../shared/globalFileNames"
|
|||
import { defaultModeSlug, getModeBySlug, getFullModeDetails, isToolAllowedForMode } from "../shared/modes"
|
||||
import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
|
||||
import { formatLanguage } from "../shared/language"
|
||||
import { ToolParamName, ToolName, ToolResponse } from "../shared/tools"
|
||||
import { ToolParamName, ToolResponse } from "../shared/tools"
|
||||
|
||||
// services
|
||||
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
|
||||
|
|
@ -106,8 +106,8 @@ export type ClineEvents = {
|
|||
taskAskResponded: []
|
||||
taskAborted: []
|
||||
taskSpawned: [taskId: string]
|
||||
taskCompleted: [taskId: string, usage: TokenUsage]
|
||||
taskTokenUsageUpdated: [taskId: string, usage: TokenUsage]
|
||||
taskCompleted: [taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage]
|
||||
taskTokenUsageUpdated: [taskId: string, tokenUsage: TokenUsage]
|
||||
}
|
||||
|
||||
export type ClineOptions = {
|
||||
|
|
@ -189,6 +189,9 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
private didAlreadyUseTool = false
|
||||
private didCompleteReadingStream = false
|
||||
|
||||
// metrics
|
||||
private toolUsage: ToolUsage = {}
|
||||
|
||||
constructor({
|
||||
provider,
|
||||
apiConfiguration,
|
||||
|
|
@ -366,19 +369,15 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
this.emit("message", { action: "updated", message: partialMessage })
|
||||
}
|
||||
|
||||
getTokenUsage() {
|
||||
const usage = getApiMetrics(combineApiRequests(combineCommandSequences(this.clineMessages.slice(1))))
|
||||
this.emit("taskTokenUsageUpdated", this.taskId, usage)
|
||||
return usage
|
||||
}
|
||||
|
||||
private async saveClineMessages() {
|
||||
try {
|
||||
const taskDir = await this.ensureTaskDirectoryExists()
|
||||
const filePath = path.join(taskDir, GlobalFileNames.uiMessages)
|
||||
await fs.writeFile(filePath, JSON.stringify(this.clineMessages))
|
||||
// combined as they are in ChatView
|
||||
const apiMetrics = this.getTokenUsage()
|
||||
|
||||
const tokenUsage = this.getTokenUsage()
|
||||
this.emit("taskTokenUsageUpdated", this.taskId, tokenUsage)
|
||||
|
||||
const taskMessage = this.clineMessages[0] // first message is always the task say
|
||||
const lastRelevantMessage =
|
||||
this.clineMessages[
|
||||
|
|
@ -403,11 +402,11 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
number: this.taskNumber,
|
||||
ts: lastRelevantMessage.ts,
|
||||
task: taskMessage.text ?? "",
|
||||
tokensIn: apiMetrics.totalTokensIn,
|
||||
tokensOut: apiMetrics.totalTokensOut,
|
||||
cacheWrites: apiMetrics.totalCacheWrites,
|
||||
cacheReads: apiMetrics.totalCacheReads,
|
||||
totalCost: apiMetrics.totalCost,
|
||||
tokensIn: tokenUsage.totalTokensIn,
|
||||
tokensOut: tokenUsage.totalTokensOut,
|
||||
cacheWrites: tokenUsage.totalCacheWrites,
|
||||
cacheReads: tokenUsage.totalCacheReads,
|
||||
totalCost: tokenUsage.totalCost,
|
||||
size: taskDirSize,
|
||||
workspace: this.cwd,
|
||||
})
|
||||
|
|
@ -2693,4 +2692,26 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
public getFileContextTracker(): FileContextTracker {
|
||||
return this.fileContextTracker
|
||||
}
|
||||
|
||||
// Metrics
|
||||
|
||||
public getTokenUsage() {
|
||||
return getApiMetrics(combineApiRequests(combineCommandSequences(this.clineMessages.slice(1))))
|
||||
}
|
||||
|
||||
public recordToolUsage({ toolName, success = true }: { toolName: ToolName; success?: boolean }) {
|
||||
if (!this.toolUsage[toolName]) {
|
||||
this.toolUsage[toolName] = { attempts: 0, failures: 0 }
|
||||
}
|
||||
|
||||
this.toolUsage[toolName].attempts++
|
||||
|
||||
if (!success) {
|
||||
this.toolUsage[toolName].failures++
|
||||
}
|
||||
}
|
||||
|
||||
public getToolUsage() {
|
||||
return this.toolUsage
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as vscode from "vscode"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
// npx jest src/core/__tests__/CodeActionProvider.test.ts
|
||||
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { CodeActionProvider, ACTION_NAMES } from "../CodeActionProvider"
|
||||
import { EditorUtils } from "../EditorUtils"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
// npx jest src/core/__tests__/EditorUtils.test.ts
|
||||
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { EditorUtils } from "../EditorUtils"
|
||||
|
||||
// Use simple classes to simulate VSCode's Range and Position behavior.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// npx jest src/core/__tests__/mode-validator.test.ts
|
||||
|
||||
import { isToolAllowedForMode, getModeConfig, modes, ModeConfig } from "../../shared/modes"
|
||||
import { TOOL_GROUPS } from "../../shared/tools"
|
||||
import { validateToolUse } from "../mode-validator"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
// npx jest src/core/__tests__/read-file-maxReadFileLine.test.ts
|
||||
|
||||
import * as path from "path"
|
||||
|
||||
import { countFileLines } from "../../integrations/misc/line-counter"
|
||||
import { readLines } from "../../integrations/misc/read-lines"
|
||||
import { extractTextFromFile, addLineNumbers } from "../../integrations/misc/extract-text"
|
||||
import { extractTextFromFile } from "../../integrations/misc/extract-text"
|
||||
import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
|
||||
import { isBinaryFile } from "isbinaryfile"
|
||||
import { ReadFileToolUse } from "../../shared/tools"
|
||||
import { ToolUsage } from "../../schemas"
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock("../../integrations/misc/line-counter")
|
||||
|
|
@ -69,7 +72,6 @@ describe("read_file tool with maxReadFileLine setting", () => {
|
|||
const mockedCountFileLines = countFileLines as jest.MockedFunction<typeof countFileLines>
|
||||
const mockedReadLines = readLines as jest.MockedFunction<typeof readLines>
|
||||
const mockedExtractTextFromFile = extractTextFromFile as jest.MockedFunction<typeof extractTextFromFile>
|
||||
const mockedAddLineNumbers = addLineNumbers as jest.MockedFunction<typeof addLineNumbers>
|
||||
const mockedParseSourceCodeDefinitionsForFile = parseSourceCodeDefinitionsForFile as jest.MockedFunction<
|
||||
typeof parseSourceCodeDefinitionsForFile
|
||||
>
|
||||
|
|
@ -125,6 +127,7 @@ describe("read_file tool with maxReadFileLine setting", () => {
|
|||
mockCline.getFileContextTracker = jest.fn().mockReturnValue({
|
||||
trackFileContext: jest.fn().mockResolvedValue(undefined),
|
||||
})
|
||||
mockCline.recordToolUsage = jest.fn().mockReturnValue({} as ToolUsage)
|
||||
|
||||
// Reset tool result
|
||||
toolResult = undefined
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// npx jest src/core/__tests__/read-file-tool.test.ts
|
||||
|
||||
import * as path from "path"
|
||||
import { countFileLines } from "../../integrations/misc/line-counter"
|
||||
import { readLines } from "../../integrations/misc/read-lines"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// npx jest src/core/__tests__/read-file-xml.test.ts
|
||||
|
||||
import * as path from "path"
|
||||
|
||||
import { countFileLines } from "../../integrations/misc/line-counter"
|
||||
|
|
@ -6,6 +8,7 @@ import { extractTextFromFile } from "../../integrations/misc/extract-text"
|
|||
import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
|
||||
import { isBinaryFile } from "isbinaryfile"
|
||||
import { ReadFileToolUse } from "../../shared/tools"
|
||||
import { ToolUsage } from "../../schemas"
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock("../../integrations/misc/line-counter")
|
||||
|
|
@ -118,6 +121,7 @@ describe("read_file tool XML output structure", () => {
|
|||
mockCline.getFileContextTracker = jest.fn().mockReturnValue({
|
||||
trackFileContext: jest.fn().mockResolvedValue(undefined),
|
||||
})
|
||||
mockCline.recordToolUsage = jest.fn().mockReturnValue({} as ToolUsage)
|
||||
|
||||
// Reset tool result
|
||||
toolResult = undefined
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { TextContent, ToolUse, ToolParamName, toolParamNames, toolNames, ToolName } from "../../shared/tools"
|
||||
import { TextContent, ToolUse, ToolParamName, toolParamNames } from "../../shared/tools"
|
||||
import { toolNames, ToolName } from "../../schemas"
|
||||
|
||||
export type AssistantMessageContent = TextContent | ToolUse
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ToolName } from "../shared/tools"
|
||||
import { ToolName } from "../schemas"
|
||||
import { Mode, isToolAllowedForMode, ModeConfig } from "../shared/modes"
|
||||
|
||||
export function validateToolUse(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
import { ToolName } from "../../../schemas"
|
||||
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../../shared/tools"
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
import { Mode, ModeConfig, getModeConfig, isToolAllowedForMode, getGroupName } from "../../../shared/modes"
|
||||
|
||||
import { ToolArgs } from "./types"
|
||||
import { getExecuteCommandDescription } from "./execute-command"
|
||||
import { getReadFileDescription } from "./read-file"
|
||||
import { getFetchInstructionsDescription } from "./fetch-instructions"
|
||||
|
|
@ -15,11 +22,6 @@ import { getUseMcpToolDescription } from "./use-mcp-tool"
|
|||
import { getAccessMcpResourceDescription } from "./access-mcp-resource"
|
||||
import { getSwitchModeDescription } from "./switch-mode"
|
||||
import { getNewTaskDescription } from "./new-task"
|
||||
import { DiffStrategy } from "../../diff/DiffStrategy"
|
||||
import { McpHub } from "../../../services/mcp/McpHub"
|
||||
import { Mode, ModeConfig, getModeConfig, isToolAllowedForMode, getGroupName } from "../../../shared/modes"
|
||||
import { ToolName, TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../../shared/tools"
|
||||
import { ToolArgs } from "./types"
|
||||
|
||||
// Map of tool names to their description functions
|
||||
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { executeCommandTool } from "../executeCommandTool"
|
|||
import { Cline } from "../../Cline"
|
||||
import { formatResponse } from "../../prompts/responses"
|
||||
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../../shared/tools"
|
||||
import { ToolUsage } from "../../../schemas"
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock("../../Cline")
|
||||
|
|
@ -40,6 +41,7 @@ describe("executeCommandTool", () => {
|
|||
// @ts-expect-error - Jest mock function type issues
|
||||
validateCommand: jest.fn().mockReturnValue(null),
|
||||
},
|
||||
recordToolUsage: jest.fn().mockReturnValue({} as ToolUsage),
|
||||
}
|
||||
|
||||
// @ts-expect-error - Jest mock function type issues
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export async function accessMcpResourceTool(
|
|||
) {
|
||||
const server_name: string | undefined = block.params.server_name
|
||||
const uri: string | undefined = block.params.uri
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
|
|
@ -20,32 +21,42 @@ export async function accessMcpResourceTool(
|
|||
serverName: removeClosingTag("server_name", server_name),
|
||||
uri: removeClosingTag("uri", uri),
|
||||
} satisfies ClineAskUseMcpServer)
|
||||
|
||||
await cline.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!server_name) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "access_mcp_resource", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("access_mcp_resource", "server_name"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!uri) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "access_mcp_resource", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("access_mcp_resource", "uri"))
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
type: "access_mcp_resource",
|
||||
serverName: server_name,
|
||||
uri,
|
||||
} satisfies ClineAskUseMcpServer)
|
||||
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
// now execute the tool
|
||||
|
||||
// Now execute the tool
|
||||
await cline.say("mcp_server_request_started")
|
||||
const resourceResult = await cline.providerRef.deref()?.getMcpHub()?.readResource(server_name, uri)
|
||||
|
||||
const resourceResultPretty =
|
||||
resourceResult?.contents
|
||||
.map((item) => {
|
||||
|
|
@ -57,15 +68,19 @@ export async function accessMcpResourceTool(
|
|||
.filter(Boolean)
|
||||
.join("\n\n") || "(Empty response)"
|
||||
|
||||
// handle images (image must contain mimetype and blob)
|
||||
// Handle images (image must contain mimetype and blob)
|
||||
let images: string[] = []
|
||||
|
||||
resourceResult?.contents.forEach((item) => {
|
||||
if (item.mimeType?.startsWith("image") && item.blob) {
|
||||
images.push(item.blob)
|
||||
}
|
||||
})
|
||||
|
||||
await cline.say("mcp_server_response", resourceResultPretty, images)
|
||||
pushToolResult(formatResponse.toolResult(resourceResultPretty, images))
|
||||
cline.recordToolUsage({ toolName: "access_mcp_resource" })
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -23,11 +23,13 @@ export async function appendToFileTool(
|
|||
) {
|
||||
const relPath: string | undefined = block.params.path
|
||||
let newContent: string | undefined = block.params.content
|
||||
|
||||
if (!relPath || !newContent) {
|
||||
return
|
||||
}
|
||||
|
||||
const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
|
||||
|
||||
if (!accessAllowed) {
|
||||
await cline.say("rooignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
|
||||
|
|
@ -48,6 +50,7 @@ export async function appendToFileTool(
|
|||
if (newContent.startsWith("```")) {
|
||||
newContent = newContent.split("\n").slice(1).join("\n").trim()
|
||||
}
|
||||
|
||||
if (newContent.endsWith("```")) {
|
||||
newContent = newContent.split("\n").slice(0, -1).join("\n").trim()
|
||||
}
|
||||
|
|
@ -68,36 +71,44 @@ export async function appendToFileTool(
|
|||
|
||||
try {
|
||||
if (block.partial) {
|
||||
// update gui message
|
||||
// Update GUI message
|
||||
const partialMessage = JSON.stringify(sharedMessageProps)
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
// update editor
|
||||
|
||||
// Update editor
|
||||
if (!cline.diffViewProvider.isEditing) {
|
||||
await cline.diffViewProvider.open(relPath)
|
||||
}
|
||||
|
||||
// If file exists, append newContent to existing content
|
||||
if (fileExists && cline.diffViewProvider.originalContent) {
|
||||
newContent = cline.diffViewProvider.originalContent + "\n" + newContent
|
||||
}
|
||||
// editor is open, stream content in
|
||||
|
||||
// Editor is open, stream content in
|
||||
await cline.diffViewProvider.update(
|
||||
everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
|
||||
false,
|
||||
)
|
||||
|
||||
return
|
||||
} else {
|
||||
if (!relPath) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "append_to_file", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("append_to_file", "path"))
|
||||
await cline.diffViewProvider.reset()
|
||||
return
|
||||
}
|
||||
|
||||
if (!newContent) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "append_to_file", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("append_to_file", "content"))
|
||||
await cline.diffViewProvider.reset()
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
if (!cline.diffViewProvider.isEditing) {
|
||||
|
|
@ -125,17 +136,21 @@ export async function appendToFileTool(
|
|||
? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent)
|
||||
: undefined,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
await cline.diffViewProvider.revertChanges()
|
||||
return
|
||||
}
|
||||
|
||||
const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges()
|
||||
|
||||
// Track file edit operation
|
||||
if (relPath) {
|
||||
await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource)
|
||||
}
|
||||
|
||||
cline.didEditFile = true
|
||||
|
||||
if (userEdits) {
|
||||
|
|
@ -147,6 +162,7 @@ export async function appendToFileTool(
|
|||
diff: userEdits,
|
||||
} satisfies ClineSayTool),
|
||||
)
|
||||
|
||||
pushToolResult(
|
||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||
|
|
@ -162,7 +178,10 @@ export async function appendToFileTool(
|
|||
} else {
|
||||
pushToolResult(`The content was successfully appended to ${relPath.toPosix()}.${newProblemsMessage}`)
|
||||
}
|
||||
|
||||
cline.recordToolUsage({ toolName: "append_to_file" })
|
||||
await cline.diffViewProvider.reset()
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -35,33 +35,36 @@ export async function applyDiffTool(
|
|||
|
||||
try {
|
||||
if (block.partial) {
|
||||
// update gui message
|
||||
// Update GUI message
|
||||
let toolProgressStatus
|
||||
|
||||
if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) {
|
||||
toolProgressStatus = cline.diffStrategy.getProgressStatus(block)
|
||||
}
|
||||
|
||||
const partialMessage = JSON.stringify(sharedMessageProps)
|
||||
|
||||
await cline.ask("tool", partialMessage, block.partial, toolProgressStatus).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!relPath) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "apply_diff", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("apply_diff", "path"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!diffContent) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "apply_diff", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("apply_diff", "diff"))
|
||||
return
|
||||
}
|
||||
|
||||
const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
|
||||
|
||||
if (!accessAllowed) {
|
||||
await cline.say("rooignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +73,7 @@ export async function applyDiffTool(
|
|||
|
||||
if (!fileExists) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "apply_diff", success: false })
|
||||
const formattedError = `File does not exist at path: ${absolutePath}\n\n<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
|
||||
await cline.say("error", formattedError)
|
||||
pushToolResult(formattedError)
|
||||
|
|
@ -87,14 +91,15 @@ export async function applyDiffTool(
|
|||
success: false,
|
||||
error: "No diff strategy available",
|
||||
}
|
||||
|
||||
let partResults = ""
|
||||
|
||||
if (!diffResult.success) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "apply_diff", success: false })
|
||||
const currentCount = (cline.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
|
||||
cline.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
|
||||
let formattedError = ""
|
||||
|
||||
telemetryService.captureDiffApplicationError(cline.taskId, currentCount)
|
||||
|
||||
if (diffResult.failParts && diffResult.failParts.length > 0) {
|
||||
|
|
@ -102,14 +107,18 @@ export async function applyDiffTool(
|
|||
if (failPart.success) {
|
||||
continue
|
||||
}
|
||||
|
||||
const errorDetails = failPart.details ? JSON.stringify(failPart.details, null, 2) : ""
|
||||
|
||||
formattedError = `<error_details>\n${
|
||||
failPart.error
|
||||
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||
|
||||
partResults += formattedError
|
||||
}
|
||||
} else {
|
||||
const errorDetails = diffResult.details ? JSON.stringify(diffResult.details, null, 2) : ""
|
||||
|
||||
formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
|
||||
diffResult.error
|
||||
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||
|
|
@ -118,12 +127,14 @@ export async function applyDiffTool(
|
|||
if (currentCount >= 2) {
|
||||
await cline.say("diff_error", formattedError)
|
||||
}
|
||||
|
||||
pushToolResult(formattedError)
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
cline.consecutiveMistakeCountForApplyDiff.delete(relPath)
|
||||
|
||||
// Show diff view before asking for approval
|
||||
cline.diffViewProvider.editType = "modify"
|
||||
await cline.diffViewProvider.open(relPath)
|
||||
|
|
@ -136,26 +147,33 @@ export async function applyDiffTool(
|
|||
} satisfies ClineSayTool)
|
||||
|
||||
let toolProgressStatus
|
||||
|
||||
if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) {
|
||||
toolProgressStatus = cline.diffStrategy.getProgressStatus(block, diffResult)
|
||||
}
|
||||
|
||||
const didApprove = await askApproval("tool", completeMessage, toolProgressStatus)
|
||||
|
||||
if (!didApprove) {
|
||||
await cline.diffViewProvider.revertChanges() // cline likely handles closing the diff view
|
||||
await cline.diffViewProvider.revertChanges() // Cline likely handles closing the diff view
|
||||
return
|
||||
}
|
||||
|
||||
const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges()
|
||||
|
||||
// Track file edit operation
|
||||
if (relPath) {
|
||||
await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource)
|
||||
}
|
||||
cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
|
||||
|
||||
// Used to determine if we should wait for busy terminal to update before sending api request
|
||||
cline.didEditFile = true
|
||||
let partFailHint = ""
|
||||
|
||||
if (diffResult.failParts && diffResult.failParts.length > 0) {
|
||||
partFailHint = `But unable to apply all diff parts to file: ${absolutePath}, silently use <read_file> tool to check newest file version and re-apply diffs\n`
|
||||
}
|
||||
|
||||
if (userEdits) {
|
||||
await cline.say(
|
||||
"user_feedback_diff",
|
||||
|
|
@ -165,6 +183,7 @@ export async function applyDiffTool(
|
|||
diff: userEdits,
|
||||
} satisfies ClineSayTool),
|
||||
)
|
||||
|
||||
pushToolResult(
|
||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||
partFailHint +
|
||||
|
|
@ -183,7 +202,10 @@ export async function applyDiffTool(
|
|||
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}\n` + partFailHint,
|
||||
)
|
||||
}
|
||||
|
||||
cline.recordToolUsage({ toolName: "apply_diff" })
|
||||
await cline.diffViewProvider.reset()
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export async function askFollowupQuestionTool(
|
|||
) {
|
||||
const question: string | undefined = block.params.question
|
||||
const follow_up: string | undefined = block.params.follow_up
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
await cline.ask("followup", removeClosingTag("question", question), block.partial).catch(() => {})
|
||||
|
|
@ -20,13 +21,12 @@ export async function askFollowupQuestionTool(
|
|||
} else {
|
||||
if (!question) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "ask_followup_question", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("ask_followup_question", "question"))
|
||||
return
|
||||
}
|
||||
|
||||
type Suggest = {
|
||||
answer: string
|
||||
}
|
||||
type Suggest = { answer: string }
|
||||
|
||||
let follow_up_json = {
|
||||
question,
|
||||
|
|
@ -39,11 +39,10 @@ export async function askFollowupQuestionTool(
|
|||
}
|
||||
|
||||
try {
|
||||
parsedSuggest = parseXml(follow_up, ["suggest"]) as {
|
||||
suggest: Suggest[] | Suggest
|
||||
}
|
||||
parsedSuggest = parseXml(follow_up, ["suggest"]) as { suggest: Suggest[] | Suggest }
|
||||
} catch (error) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "ask_followup_question", success: false })
|
||||
await cline.say("error", `Failed to parse operations: ${error.message}`)
|
||||
pushToolResult(formatResponse.toolError("Invalid operations xml format"))
|
||||
return
|
||||
|
|
@ -57,10 +56,11 @@ export async function askFollowupQuestionTool(
|
|||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
const { text, images } = await cline.ask("followup", JSON.stringify(follow_up_json), false)
|
||||
await cline.say("user_feedback", text ?? "", images)
|
||||
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
|
||||
cline.recordToolUsage({ toolName: "ask_followup_question" })
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ export async function attemptCompletionTool(
|
|||
) {
|
||||
const result: string | undefined = block.params.result
|
||||
const command: string | undefined = block.params.command
|
||||
|
||||
try {
|
||||
const lastMessage = cline.clineMessages.at(-1)
|
||||
|
||||
if (block.partial) {
|
||||
if (command) {
|
||||
// the attempt_completion text is done, now we're getting command
|
||||
|
|
@ -43,7 +45,7 @@ export async function attemptCompletionTool(
|
|||
await cline.say("completion_result", removeClosingTag("result", result), undefined, false)
|
||||
|
||||
telemetryService.captureTaskCompleted(cline.taskId)
|
||||
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage())
|
||||
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.getToolUsage())
|
||||
|
||||
await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {})
|
||||
}
|
||||
|
|
@ -55,6 +57,7 @@ export async function attemptCompletionTool(
|
|||
} else {
|
||||
if (!result) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "attempt_completion", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("attempt_completion", "result"))
|
||||
return
|
||||
}
|
||||
|
|
@ -68,7 +71,7 @@ export async function attemptCompletionTool(
|
|||
// Haven't sent a command message yet so first send completion_result then command.
|
||||
await cline.say("completion_result", result, undefined, false)
|
||||
telemetryService.captureTaskCompleted(cline.taskId)
|
||||
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage())
|
||||
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.getToolUsage())
|
||||
}
|
||||
|
||||
// Complete command message.
|
||||
|
|
@ -91,7 +94,7 @@ export async function attemptCompletionTool(
|
|||
} else {
|
||||
await cline.say("completion_result", result, undefined, false)
|
||||
telemetryService.captureTaskCompleted(cline.taskId)
|
||||
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage())
|
||||
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.getToolUsage())
|
||||
}
|
||||
|
||||
if (cline.parentTask) {
|
||||
|
|
@ -136,13 +139,10 @@ export async function attemptCompletionTool(
|
|||
})
|
||||
|
||||
toolResults.push(...formatResponse.imageBlocks(images))
|
||||
|
||||
cline.userMessageContent.push({
|
||||
type: "text",
|
||||
text: `${toolDescription()} Result:`,
|
||||
})
|
||||
|
||||
cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` })
|
||||
cline.userMessageContent.push(...toolResults)
|
||||
cline.recordToolUsage({ toolName: "attempt_completion" })
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -21,14 +21,17 @@ export async function browserActionTool(
|
|||
const coordinate: string | undefined = block.params.coordinate
|
||||
const text: string | undefined = block.params.text
|
||||
const size: string | undefined = block.params.size
|
||||
|
||||
if (!action || !browserActions.includes(action)) {
|
||||
// checking for action to ensure it is complete and valid
|
||||
if (!block.partial) {
|
||||
// if the block is complete and we don't have a valid action cline is a mistake
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "browser_action", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "action"))
|
||||
await cline.browserSession.closeBrowser()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -52,51 +55,63 @@ export async function browserActionTool(
|
|||
} else {
|
||||
// Initialize with empty object to avoid "used before assigned" errors
|
||||
let browserActionResult: BrowserActionResult = {}
|
||||
|
||||
if (action === "launch") {
|
||||
if (!url) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "browser_action", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "url"))
|
||||
await cline.browserSession.closeBrowser()
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
const didApprove = await askApproval("browser_action_launch", url)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: it's okay that we call cline message since the partial inspect_site is finished streaming. The only scenario we have to avoid is sending messages WHILE a partial message exists at the end of the messages array. For example the api_req_finished message would interfere with the partial message, so we needed to remove that.
|
||||
// await cline.say("inspect_site_result", "") // no result, starts the loading spinner waiting for result
|
||||
await cline.say("browser_action_result", "") // starts loading spinner
|
||||
|
||||
// NOTE: It's okay that we call cline message since the partial inspect_site is finished streaming.
|
||||
// The only scenario we have to avoid is sending messages WHILE a partial message exists at the end of the messages array.
|
||||
// For example the api_req_finished message would interfere with the partial message, so we needed to remove that.
|
||||
// await cline.say("inspect_site_result", "") // No result, starts the loading spinner waiting for result
|
||||
await cline.say("browser_action_result", "") // Starts loading spinner
|
||||
await cline.browserSession.launchBrowser()
|
||||
browserActionResult = await cline.browserSession.navigateToUrl(url)
|
||||
} else {
|
||||
if (action === "click" || action === "hover") {
|
||||
if (!coordinate) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "browser_action", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "coordinate"))
|
||||
await cline.browserSession.closeBrowser()
|
||||
return // can't be within an inner switch
|
||||
}
|
||||
}
|
||||
|
||||
if (action === "type") {
|
||||
if (!text) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "browser_action", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "text"))
|
||||
await cline.browserSession.closeBrowser()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (action === "resize") {
|
||||
if (!size) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "browser_action", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("browser_action", "size"))
|
||||
await cline.browserSession.closeBrowser()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
await cline.say(
|
||||
"browser_action",
|
||||
JSON.stringify({
|
||||
|
|
@ -107,6 +122,7 @@ export async function browserActionTool(
|
|||
undefined,
|
||||
false,
|
||||
)
|
||||
|
||||
switch (action) {
|
||||
case "click":
|
||||
browserActionResult = await cline.browserSession.click(coordinate!)
|
||||
|
|
@ -141,6 +157,7 @@ export async function browserActionTool(
|
|||
case "scroll_up":
|
||||
case "resize":
|
||||
await cline.say("browser_action_result", JSON.stringify(browserActionResult))
|
||||
|
||||
pushToolResult(
|
||||
formatResponse.toolResult(
|
||||
`The browser action has been executed. The console logs and screenshot have been captured for your analysis.\n\nConsole logs:\n${
|
||||
|
|
@ -149,6 +166,7 @@ export async function browserActionTool(
|
|||
browserActionResult?.screenshot ? [browserActionResult.screenshot] : [],
|
||||
),
|
||||
)
|
||||
|
||||
break
|
||||
case "close":
|
||||
pushToolResult(
|
||||
|
|
@ -156,8 +174,12 @@ export async function browserActionTool(
|
|||
`The browser has been closed. You may now proceed to using other tools.`,
|
||||
),
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
cline.recordToolUsage({ toolName: "browser_action" })
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export async function executeCommandTool(
|
|||
) {
|
||||
let command: string | undefined = block.params.command
|
||||
const customCwd: string | undefined = block.params.cwd
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {})
|
||||
|
|
@ -20,6 +21,7 @@ export async function executeCommandTool(
|
|||
} else {
|
||||
if (!command) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "execute_command", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("execute_command", "command"))
|
||||
return
|
||||
}
|
||||
|
|
@ -28,7 +30,6 @@ export async function executeCommandTool(
|
|||
if (ignoredFileAttemptedToAccess) {
|
||||
await cline.say("rooignore_error", ignoredFileAttemptedToAccess)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -38,14 +39,20 @@ export async function executeCommandTool(
|
|||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
const didApprove = await askApproval("command", command)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
const [userRejected, result] = await cline.executeCommandTool(command, customCwd)
|
||||
|
||||
if (userRejected) {
|
||||
cline.didRejectTool = true
|
||||
}
|
||||
|
||||
pushToolResult(result)
|
||||
cline.recordToolUsage({ toolName: "execute_command" })
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -12,50 +12,51 @@ export async function fetchInstructionsTool(
|
|||
pushToolResult: PushToolResult,
|
||||
) {
|
||||
const task: string | undefined = block.params.task
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "fetchInstructions",
|
||||
content: task,
|
||||
}
|
||||
const sharedMessageProps: ClineSayTool = { tool: "fetchInstructions", content: task }
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: undefined,
|
||||
} satisfies ClineSayTool)
|
||||
const partialMessage = JSON.stringify({ ...sharedMessageProps, content: undefined } satisfies ClineSayTool)
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!task) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "fetch_instructions", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("fetch_instructions", "task"))
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: task,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
const completeMessage = JSON.stringify({ ...sharedMessageProps, content: task } satisfies ClineSayTool)
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
// now fetch the content and provide it to the agent.
|
||||
// Bow fetch the content and provide it to the agent.
|
||||
const provider = cline.providerRef.deref()
|
||||
const mcpHub = provider?.getMcpHub()
|
||||
|
||||
if (!mcpHub) {
|
||||
throw new Error("MCP hub not available")
|
||||
}
|
||||
|
||||
const diffStrategy = cline.diffStrategy
|
||||
const context = provider?.context
|
||||
const content = await fetchInstructions(task, { mcpHub, diffStrategy, context })
|
||||
|
||||
if (!content) {
|
||||
pushToolResult(formatResponse.toolError(`Invalid instructions request: ${task}`))
|
||||
return
|
||||
}
|
||||
|
||||
pushToolResult(content)
|
||||
cline.recordToolUsage({ toolName: "fetch_instructions" })
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("fetch instructions", error)
|
||||
|
|
|
|||
|
|
@ -37,12 +37,14 @@ export async function insertContentTool(
|
|||
// Validate required parameters
|
||||
if (!relPath) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "insert_content", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "path"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!operations) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "insert_content", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "operations"))
|
||||
return
|
||||
}
|
||||
|
|
@ -52,6 +54,7 @@ export async function insertContentTool(
|
|||
|
||||
if (!fileExists) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "insert_content", success: false })
|
||||
const formattedError = `File does not exist at path: ${absolutePath}\n\n<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
|
||||
await cline.say("error", formattedError)
|
||||
pushToolResult(formattedError)
|
||||
|
|
@ -70,6 +73,7 @@ export async function insertContentTool(
|
|||
}
|
||||
} catch (error) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "insert_content", success: false })
|
||||
await cline.say("error", `Failed to parse operations JSON: ${error.message}`)
|
||||
pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
|
||||
return
|
||||
|
|
@ -112,10 +116,7 @@ export async function insertContentTool(
|
|||
|
||||
await cline.diffViewProvider.update(updatedContent, true)
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
diff,
|
||||
} satisfies ClineSayTool)
|
||||
const completeMessage = JSON.stringify({ ...sharedMessageProps, diff } satisfies ClineSayTool)
|
||||
|
||||
const didApprove = await cline
|
||||
.ask("tool", completeMessage, false)
|
||||
|
|
@ -133,6 +134,7 @@ export async function insertContentTool(
|
|||
if (relPath) {
|
||||
await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource)
|
||||
}
|
||||
|
||||
cline.didEditFile = true
|
||||
|
||||
if (!userEdits) {
|
||||
|
|
@ -149,6 +151,7 @@ export async function insertContentTool(
|
|||
|
||||
console.debug("[DEBUG] User made edits, sending feedback diff:", userFeedbackDiff)
|
||||
await cline.say("user_feedback_diff", userFeedbackDiff)
|
||||
|
||||
pushToolResult(
|
||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file:\n\n` +
|
||||
|
|
@ -159,6 +162,8 @@ export async function insertContentTool(
|
|||
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
|
||||
`${newProblemsMessage}`,
|
||||
)
|
||||
|
||||
cline.recordToolUsage({ toolName: "insert_content" })
|
||||
await cline.diffViewProvider.reset()
|
||||
} catch (error) {
|
||||
handleError("insert content", error)
|
||||
|
|
|
|||
|
|
@ -17,29 +17,33 @@ export async function listCodeDefinitionNamesTool(
|
|||
removeClosingTag: RemoveClosingTag,
|
||||
) {
|
||||
const relPath: string | undefined = block.params.path
|
||||
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "listCodeDefinitionNames",
|
||||
path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
|
||||
}
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: "",
|
||||
} satisfies ClineSayTool)
|
||||
const partialMessage = JSON.stringify({ ...sharedMessageProps, content: "" } satisfies ClineSayTool)
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!relPath) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "list_code_definition_names", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("list_code_definition_names", "path"))
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
const absolutePath = path.resolve(cline.cwd, relPath)
|
||||
let result: string
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(absolutePath)
|
||||
|
||||
if (stats.isFile()) {
|
||||
const fileResult = await parseSourceCodeDefinitionsForFile(absolutePath, cline.rooIgnoreController)
|
||||
result = fileResult ?? "No source code definitions found in cline file."
|
||||
|
|
@ -51,18 +55,20 @@ export async function listCodeDefinitionNamesTool(
|
|||
} catch {
|
||||
result = `${absolutePath}: does not exist or cannot be accessed.`
|
||||
}
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: result,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result } satisfies ClineSayTool)
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
if (relPath) {
|
||||
await cline.getFileContextTracker().trackFileContext(relPath, "read_tool" as RecordSource)
|
||||
}
|
||||
|
||||
pushToolResult(result)
|
||||
cline.recordToolUsage({ toolName: "list_code_definition_names" })
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } f
|
|||
* conversation.
|
||||
* @param removeClosingTag - A function that removes a closing tag from a string.
|
||||
*/
|
||||
|
||||
export async function listFilesTool(
|
||||
cline: Cline,
|
||||
block: ToolUse,
|
||||
|
|
@ -32,28 +33,31 @@ export async function listFilesTool(
|
|||
const relDirPath: string | undefined = block.params.path
|
||||
const recursiveRaw: string | undefined = block.params.recursive
|
||||
const recursive = recursiveRaw?.toLowerCase() === "true"
|
||||
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: !recursive ? "listFilesTopLevel" : "listFilesRecursive",
|
||||
path: getReadablePath(cline.cwd, removeClosingTag("path", relDirPath)),
|
||||
}
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: "",
|
||||
} satisfies ClineSayTool)
|
||||
const partialMessage = JSON.stringify({ ...sharedMessageProps, content: "" } satisfies ClineSayTool)
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!relDirPath) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "list_files", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("list_files", "path"))
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
const absolutePath = path.resolve(cline.cwd, relDirPath)
|
||||
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
|
||||
const { showRooIgnoredFiles = true } = (await cline.providerRef.deref()?.getState()) ?? {}
|
||||
|
||||
const result = formatResponse.formatFilesList(
|
||||
absolutePath,
|
||||
files,
|
||||
|
|
@ -61,15 +65,16 @@ export async function listFilesTool(
|
|||
cline.rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
)
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: result,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
const completeMessage = JSON.stringify({ ...sharedMessageProps, content: result } satisfies ClineSayTool)
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
pushToolResult(result)
|
||||
cline.recordToolUsage({ toolName: "list_files" })
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("listing files", error)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export async function newTaskTool(
|
|||
) {
|
||||
const mode: string | undefined = block.params.mode
|
||||
const message: string | undefined = block.params.message
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
|
|
@ -22,23 +23,29 @@ export async function newTaskTool(
|
|||
mode: removeClosingTag("mode", mode),
|
||||
message: removeClosingTag("message", message),
|
||||
})
|
||||
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!mode) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "new_task", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("new_task", "mode"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "new_task", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("new_task", "message"))
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
// Verify the mode exists
|
||||
const targetMode = getModeBySlug(mode, (await cline.providerRef.deref()?.getState())?.customModes)
|
||||
|
||||
if (!targetMode) {
|
||||
pushToolResult(formatResponse.toolError(`Invalid mode: ${mode}`))
|
||||
return
|
||||
|
|
@ -49,6 +56,7 @@ export async function newTaskTool(
|
|||
mode: targetMode.name,
|
||||
content: message,
|
||||
})
|
||||
|
||||
const didApprove = await askApproval("tool", toolMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
|
|
@ -74,6 +82,7 @@ export async function newTaskTool(
|
|||
cline.emit("taskSpawned", newCline.taskId)
|
||||
|
||||
pushToolResult(`Successfully created new task in ${targetMode.name} mode with message: ${message}`)
|
||||
cline.recordToolUsage({ toolName: "new_task" })
|
||||
|
||||
// Set the isPaused flag to true so the parent
|
||||
// task can wait for the sub-task to finish.
|
||||
|
|
|
|||
|
|
@ -37,15 +37,13 @@ export async function readFileTool(
|
|||
}
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: undefined,
|
||||
} satisfies ClineSayTool)
|
||||
const partialMessage = JSON.stringify({ ...sharedMessageProps, content: undefined } satisfies ClineSayTool)
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!relPath) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "read_file", success: false })
|
||||
const errorMsg = await cline.sayAndCreateMissingParamError("read_file", "path")
|
||||
pushToolResult(`<file><path></path><error>${errorMsg}</error></file>`)
|
||||
return
|
||||
|
|
@ -67,13 +65,16 @@ export async function readFileTool(
|
|||
// Parse start_line if provided
|
||||
if (startLineStr) {
|
||||
startLine = parseInt(startLineStr)
|
||||
|
||||
if (isNaN(startLine)) {
|
||||
// Invalid start_line
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "read_file", success: false })
|
||||
await cline.say("error", `Failed to parse start_line: ${startLineStr}`)
|
||||
pushToolResult(`<file><path>${relPath}</path><error>Invalid start_line value</error></file>`)
|
||||
return
|
||||
}
|
||||
|
||||
startLine -= 1 // Convert to 0-based index
|
||||
}
|
||||
|
||||
|
|
@ -84,6 +85,7 @@ export async function readFileTool(
|
|||
if (isNaN(endLine)) {
|
||||
// Invalid end_line
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "read_file", success: false })
|
||||
await cline.say("error", `Failed to parse end_line: ${endLineStr}`)
|
||||
pushToolResult(`<file><path>${relPath}</path><error>Invalid end_line value</error></file>`)
|
||||
return
|
||||
|
|
@ -94,6 +96,7 @@ export async function readFileTool(
|
|||
}
|
||||
|
||||
const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
|
||||
|
||||
if (!accessAllowed) {
|
||||
await cline.say("rooignore_error", relPath)
|
||||
const errorMsg = formatResponse.rooIgnoreError(relPath)
|
||||
|
|
@ -103,6 +106,7 @@ export async function readFileTool(
|
|||
|
||||
// Create line snippet description for approval message
|
||||
let lineSnippet = ""
|
||||
|
||||
if (isFullRead) {
|
||||
// No snippet for full read
|
||||
} else if (startLine !== undefined && endLine !== undefined) {
|
||||
|
|
@ -127,12 +131,14 @@ export async function readFileTool(
|
|||
} satisfies ClineSayTool)
|
||||
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
// Count total lines in the file
|
||||
let totalLines = 0
|
||||
|
||||
try {
|
||||
totalLines = await countFileLines(absolutePath)
|
||||
} catch (error) {
|
||||
|
|
@ -163,6 +169,7 @@ export async function readFileTool(
|
|||
|
||||
content = res[0].length > 0 ? addLineNumbers(res[0]) : ""
|
||||
const result = res[1]
|
||||
|
||||
if (result) {
|
||||
sourceCodeDef = `${result}`
|
||||
}
|
||||
|
|
@ -211,9 +218,11 @@ export async function readFileTool(
|
|||
else {
|
||||
// For non-range reads, always show line range
|
||||
let lines = totalLines
|
||||
|
||||
if (maxReadFileLine >= 0 && totalLines > maxReadFileLine) {
|
||||
lines = maxReadFileLine
|
||||
}
|
||||
|
||||
const lineRangeAttr = ` lines="1-${lines}"`
|
||||
|
||||
// Maintain exact format expected by tests
|
||||
|
|
@ -228,6 +237,7 @@ export async function readFileTool(
|
|||
// Format the result into the required XML structure
|
||||
const xmlResult = `<file><path>${relPath}</path>\n${contentTag}${xmlInfo}</file>`
|
||||
pushToolResult(xmlResult)
|
||||
cline.recordToolUsage({ toolName: "read_file" })
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
|
|
|
|||
|
|
@ -32,16 +32,20 @@ export async function searchAndReplaceTool(
|
|||
path: removeClosingTag("path", relPath),
|
||||
operations: removeClosingTag("operations", operations),
|
||||
})
|
||||
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!relPath) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "search_and_replace", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("search_and_replace", "path"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!operations) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "search_and_replace", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("search_and_replace", "operations"))
|
||||
return
|
||||
}
|
||||
|
|
@ -51,6 +55,7 @@ export async function searchAndReplaceTool(
|
|||
|
||||
if (!fileExists) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "search_and_replace", success: false })
|
||||
const formattedError = `File does not exist at path: ${absolutePath}\n\n<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
|
||||
await cline.say("error", formattedError)
|
||||
pushToolResult(formattedError)
|
||||
|
|
@ -69,11 +74,13 @@ export async function searchAndReplaceTool(
|
|||
|
||||
try {
|
||||
parsedOperations = JSON.parse(operations)
|
||||
|
||||
if (!Array.isArray(parsedOperations)) {
|
||||
throw new Error("Operations must be an array")
|
||||
}
|
||||
} catch (error) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "search_and_replace", success: false })
|
||||
await cline.say("error", `Failed to parse operations JSON: ${error.message}`)
|
||||
pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
|
||||
return
|
||||
|
|
@ -132,18 +139,16 @@ export async function searchAndReplaceTool(
|
|||
await cline.diffViewProvider.update(newContent, true)
|
||||
cline.diffViewProvider.scrollToFirstDiff()
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
diff: diff,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
const completeMessage = JSON.stringify({ ...sharedMessageProps, diff: diff } satisfies ClineSayTool)
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
await cline.diffViewProvider.revertChanges() // cline likely handles closing the diff view
|
||||
return
|
||||
}
|
||||
|
||||
const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges()
|
||||
|
||||
if (relPath) {
|
||||
await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource)
|
||||
}
|
||||
|
|
@ -158,6 +163,7 @@ export async function searchAndReplaceTool(
|
|||
diff: userEdits,
|
||||
} satisfies ClineSayTool),
|
||||
)
|
||||
|
||||
pushToolResult(
|
||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||
|
|
@ -171,7 +177,10 @@ export async function searchAndReplaceTool(
|
|||
} else {
|
||||
pushToolResult(`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`)
|
||||
}
|
||||
|
||||
cline.recordToolUsage({ toolName: "search_and_replace" })
|
||||
await cline.diffViewProvider.reset()
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -17,33 +17,38 @@ export async function searchFilesTool(
|
|||
const relDirPath: string | undefined = block.params.path
|
||||
const regex: string | undefined = block.params.regex
|
||||
const filePattern: string | undefined = block.params.file_pattern
|
||||
|
||||
const sharedMessageProps: ClineSayTool = {
|
||||
tool: "searchFiles",
|
||||
path: getReadablePath(cline.cwd, removeClosingTag("path", relDirPath)),
|
||||
regex: removeClosingTag("regex", regex),
|
||||
filePattern: removeClosingTag("file_pattern", filePattern),
|
||||
}
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: "",
|
||||
} satisfies ClineSayTool)
|
||||
const partialMessage = JSON.stringify({ ...sharedMessageProps, content: "" } satisfies ClineSayTool)
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!relDirPath) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "search_files", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("search_files", "path"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!regex) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "search_files", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("search_files", "regex"))
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
const absolutePath = path.resolve(cline.cwd, relDirPath)
|
||||
|
||||
const results = await regexSearchFiles(
|
||||
cline.cwd,
|
||||
absolutePath,
|
||||
|
|
@ -51,15 +56,17 @@ export async function searchFilesTool(
|
|||
filePattern,
|
||||
cline.rooIgnoreController,
|
||||
)
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: results,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
const completeMessage = JSON.stringify({ ...sharedMessageProps, content: results } satisfies ClineSayTool)
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
pushToolResult(results)
|
||||
cline.recordToolUsage({ toolName: "search_files" })
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export async function switchModeTool(
|
|||
) {
|
||||
const mode_slug: string | undefined = block.params.mode_slug
|
||||
const reason: string | undefined = block.params.reason
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
|
|
@ -22,49 +23,57 @@ export async function switchModeTool(
|
|||
mode: removeClosingTag("mode_slug", mode_slug),
|
||||
reason: removeClosingTag("reason", reason),
|
||||
})
|
||||
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!mode_slug) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "switch_mode", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("switch_mode", "mode_slug"))
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
// Verify the mode exists
|
||||
const targetMode = getModeBySlug(mode_slug, (await cline.providerRef.deref()?.getState())?.customModes)
|
||||
|
||||
if (!targetMode) {
|
||||
cline.recordToolUsage({ toolName: "switch_mode", success: false })
|
||||
pushToolResult(formatResponse.toolError(`Invalid mode: ${mode_slug}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already in requested mode
|
||||
const currentMode = (await cline.providerRef.deref()?.getState())?.mode ?? defaultModeSlug
|
||||
|
||||
if (currentMode === mode_slug) {
|
||||
cline.recordToolUsage({ toolName: "switch_mode", success: false })
|
||||
pushToolResult(`Already in ${targetMode.name} mode.`)
|
||||
return
|
||||
}
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
tool: "switchMode",
|
||||
mode: mode_slug,
|
||||
reason,
|
||||
})
|
||||
|
||||
const completeMessage = JSON.stringify({ tool: "switchMode", mode: mode_slug, reason })
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
// Switch the mode using shared handler
|
||||
await cline.providerRef.deref()?.handleModeSwitch(mode_slug)
|
||||
|
||||
pushToolResult(
|
||||
`Successfully switched from ${getModeBySlug(currentMode)?.name ?? currentMode} mode to ${
|
||||
targetMode.name
|
||||
} mode${reason ? ` because: ${reason}` : ""}.`,
|
||||
)
|
||||
await delay(500) // delay to allow mode change to take effect before next tool is executed
|
||||
|
||||
cline.recordToolUsage({ toolName: "switch_mode" })
|
||||
|
||||
await delay(500) // Delay to allow mode change to take effect before next tool is executed
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -22,51 +22,60 @@ export async function useMcpToolTool(
|
|||
toolName: removeClosingTag("tool_name", tool_name),
|
||||
arguments: removeClosingTag("arguments", mcp_arguments),
|
||||
} satisfies ClineAskUseMcpServer)
|
||||
|
||||
await cline.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!server_name) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "switch_mode", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "server_name"))
|
||||
return
|
||||
}
|
||||
|
||||
if (!tool_name) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "use_mcp_tool", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"))
|
||||
return
|
||||
}
|
||||
// arguments are optional, but if they are provided they must be valid JSON
|
||||
// if (!mcp_arguments) {
|
||||
// cline.consecutiveMistakeCount++
|
||||
// pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "arguments"))
|
||||
// return
|
||||
// }
|
||||
|
||||
let parsedArguments: Record<string, unknown> | undefined
|
||||
|
||||
if (mcp_arguments) {
|
||||
try {
|
||||
parsedArguments = JSON.parse(mcp_arguments)
|
||||
} catch (error) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "use_mcp_tool", success: false })
|
||||
await cline.say("error", `Roo tried to use ${tool_name} with an invalid JSON argument. Retrying...`)
|
||||
|
||||
pushToolResult(
|
||||
formatResponse.toolError(formatResponse.invalidMcpToolArgumentError(server_name, tool_name)),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
type: "use_mcp_tool",
|
||||
serverName: server_name,
|
||||
toolName: tool_name,
|
||||
arguments: mcp_arguments,
|
||||
} satisfies ClineAskUseMcpServer)
|
||||
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
// now execute the tool
|
||||
|
||||
// Now execute the tool
|
||||
await cline.say("mcp_server_request_started") // same as browser_action_result
|
||||
|
||||
const toolResult = await cline.providerRef
|
||||
.deref()
|
||||
?.getMcpHub()
|
||||
|
|
@ -88,8 +97,11 @@ export async function useMcpToolTool(
|
|||
})
|
||||
.filter(Boolean)
|
||||
.join("\n\n") || "(No response)"
|
||||
|
||||
await cline.say("mcp_server_response", toolResultPretty)
|
||||
pushToolResult(formatResponse.toolResult(toolResultPretty))
|
||||
cline.recordToolUsage({ toolName: "use_mcp_tool" })
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export async function writeToFileTool(
|
|||
const relPath: string | undefined = block.params.path
|
||||
let newContent: string | undefined = block.params.content
|
||||
let predictedLineCount: number | undefined = parseInt(block.params.line_count ?? "0")
|
||||
|
||||
if (!relPath || !newContent) {
|
||||
// checking for newContent ensure relPath is complete
|
||||
// wait so we can determine if it's a new file or editing an existing file
|
||||
|
|
@ -32,15 +33,16 @@ export async function writeToFileTool(
|
|||
}
|
||||
|
||||
const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
|
||||
|
||||
if (!accessAllowed) {
|
||||
await cline.say("rooignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Check if file exists using cached map or fs.access
|
||||
let fileExists: boolean
|
||||
|
||||
if (cline.diffViewProvider.editType !== undefined) {
|
||||
fileExists = cline.diffViewProvider.editType === "modify"
|
||||
} else {
|
||||
|
|
@ -54,6 +56,7 @@ export async function writeToFileTool(
|
|||
// cline handles cases where it includes language specifiers like ```python ```js
|
||||
newContent = newContent.split("\n").slice(1).join("\n").trim()
|
||||
}
|
||||
|
||||
if (newContent.endsWith("```")) {
|
||||
newContent = newContent.split("\n").slice(0, -1).join("\n").trim()
|
||||
}
|
||||
|
|
@ -71,41 +74,51 @@ export async function writeToFileTool(
|
|||
path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
|
||||
isOutsideWorkspace,
|
||||
}
|
||||
|
||||
try {
|
||||
if (block.partial) {
|
||||
// update gui message
|
||||
const partialMessage = JSON.stringify(sharedMessageProps)
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
|
||||
// update editor
|
||||
if (!cline.diffViewProvider.isEditing) {
|
||||
// open the editor and prepare to stream content in
|
||||
await cline.diffViewProvider.open(relPath)
|
||||
}
|
||||
|
||||
// editor is open, stream content in
|
||||
await cline.diffViewProvider.update(
|
||||
everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
|
||||
false,
|
||||
)
|
||||
|
||||
return
|
||||
} else {
|
||||
if (!relPath) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "write_to_file", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "path"))
|
||||
await cline.diffViewProvider.reset()
|
||||
return
|
||||
}
|
||||
|
||||
if (!newContent) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "write_to_file", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "content"))
|
||||
await cline.diffViewProvider.reset()
|
||||
return
|
||||
}
|
||||
|
||||
if (!predictedLineCount) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolUsage({ toolName: "write_to_file", success: false })
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "line_count"))
|
||||
await cline.diffViewProvider.reset()
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
// if isEditingFile false, that means we have the full contents of the file already.
|
||||
|
|
@ -117,10 +130,12 @@ export async function writeToFileTool(
|
|||
await cline.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, cline shows the edit row before the content is streamed into the editor
|
||||
await cline.diffViewProvider.open(relPath)
|
||||
}
|
||||
|
||||
await cline.diffViewProvider.update(
|
||||
everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
|
||||
true,
|
||||
)
|
||||
|
||||
await delay(300) // wait for diff view to update
|
||||
cline.diffViewProvider.scrollToFirstDiff()
|
||||
|
||||
|
|
@ -128,6 +143,7 @@ export async function writeToFileTool(
|
|||
if (detectCodeOmission(cline.diffViewProvider.originalContent || "", newContent, predictedLineCount)) {
|
||||
if (cline.diffStrategy) {
|
||||
await cline.diffViewProvider.revertChanges()
|
||||
|
||||
pushToolResult(
|
||||
formatResponse.toolError(
|
||||
`Content appears to be truncated (file has ${
|
||||
|
|
@ -161,18 +177,23 @@ export async function writeToFileTool(
|
|||
? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent)
|
||||
: undefined,
|
||||
} satisfies ClineSayTool)
|
||||
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
await cline.diffViewProvider.revertChanges()
|
||||
return
|
||||
}
|
||||
|
||||
const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges()
|
||||
|
||||
// Track file edit operation
|
||||
if (relPath) {
|
||||
await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource)
|
||||
}
|
||||
|
||||
cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
|
||||
|
||||
if (userEdits) {
|
||||
await cline.say(
|
||||
"user_feedback_diff",
|
||||
|
|
@ -182,6 +203,7 @@ export async function writeToFileTool(
|
|||
diff: userEdits,
|
||||
} satisfies ClineSayTool),
|
||||
)
|
||||
|
||||
pushToolResult(
|
||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||
|
|
@ -197,7 +219,10 @@ export async function writeToFileTool(
|
|||
} else {
|
||||
pushToolResult(`The content was successfully saved to ${relPath.toPosix()}.${newProblemsMessage}`)
|
||||
}
|
||||
|
||||
cline.recordToolUsage({ toolName: "write_to_file" })
|
||||
await cline.diffViewProvider.reset()
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -296,12 +296,12 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
|
|||
this.taskMap.delete(cline.taskId)
|
||||
})
|
||||
|
||||
cline.on("taskCompleted", async (_, usage) => {
|
||||
this.emit(RooCodeEventName.TaskCompleted, cline.taskId, usage)
|
||||
cline.on("taskCompleted", async (_, tokenUsage, toolUsage) => {
|
||||
this.emit(RooCodeEventName.TaskCompleted, cline.taskId, tokenUsage, toolUsage)
|
||||
this.taskMap.delete(cline.taskId)
|
||||
|
||||
await this.fileLog(
|
||||
`[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(usage, null, 2)}\n`,
|
||||
`[${new Date().toISOString()}] taskCompleted -> ${cline.taskId} | ${JSON.stringify(tokenUsage, null, 2)} | ${JSON.stringify(toolUsage, null, 2)}\n`,
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
6
src/exports/roo-code.d.ts
vendored
6
src/exports/roo-code.d.ts
vendored
|
|
@ -520,6 +520,12 @@ type RooCodeEvents = {
|
|||
totalCost: number
|
||||
contextTokens: number
|
||||
},
|
||||
{
|
||||
[x: string]: {
|
||||
attempts: number
|
||||
failures: number
|
||||
}
|
||||
},
|
||||
]
|
||||
taskTokenUsageUpdated: [
|
||||
string,
|
||||
|
|
|
|||
|
|
@ -529,6 +529,12 @@ type RooCodeEvents = {
|
|||
totalCost: number
|
||||
contextTokens: number
|
||||
},
|
||||
{
|
||||
[x: string]: {
|
||||
attempts: number
|
||||
failures: number
|
||||
}
|
||||
},
|
||||
]
|
||||
taskTokenUsageUpdated: [
|
||||
string,
|
||||
|
|
|
|||
|
|
@ -817,6 +817,45 @@ export const tokenUsageSchema = z.object({
|
|||
|
||||
export type TokenUsage = z.infer<typeof tokenUsageSchema>
|
||||
|
||||
export const toolNames = [
|
||||
"execute_command",
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"append_to_file",
|
||||
"apply_diff",
|
||||
"insert_content",
|
||||
"search_and_replace",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
"switch_mode",
|
||||
"new_task",
|
||||
"fetch_instructions",
|
||||
] as const
|
||||
|
||||
export const toolNamesSchema = z.enum(toolNames)
|
||||
|
||||
export type ToolName = z.infer<typeof toolNamesSchema>
|
||||
|
||||
/**
|
||||
* ToolUsage
|
||||
*/
|
||||
|
||||
export const toolUsageSchema = z.record(
|
||||
toolNamesSchema,
|
||||
z.object({
|
||||
attempts: z.number(),
|
||||
failures: z.number(),
|
||||
}),
|
||||
)
|
||||
|
||||
export type ToolUsage = z.infer<typeof toolUsageSchema>
|
||||
|
||||
/**
|
||||
* RooCodeEvent
|
||||
*/
|
||||
|
|
@ -851,7 +890,7 @@ export const rooCodeEventsSchema = z.object({
|
|||
[RooCodeEventName.TaskAskResponded]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskAborted]: z.tuple([z.string()]),
|
||||
[RooCodeEventName.TaskSpawned]: z.tuple([z.string(), z.string()]),
|
||||
[RooCodeEventName.TaskCompleted]: z.tuple([z.string(), tokenUsageSchema]),
|
||||
[RooCodeEventName.TaskCompleted]: z.tuple([z.string(), tokenUsageSchema, toolUsageSchema]),
|
||||
[RooCodeEventName.TaskTokenUsageUpdated]: z.tuple([z.string(), tokenUsageSchema]),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import { ClineAsk, ToolProgressStatus, ToolGroup } from "../schemas"
|
||||
import { ClineAsk, ToolProgressStatus, ToolGroup, ToolName } from "../schemas"
|
||||
|
||||
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
|
||||
|
|
@ -26,29 +26,6 @@ export interface TextContent {
|
|||
partial: boolean
|
||||
}
|
||||
|
||||
export const toolNames = [
|
||||
"execute_command",
|
||||
"read_file",
|
||||
"write_to_file",
|
||||
"append_to_file",
|
||||
"apply_diff",
|
||||
"insert_content",
|
||||
"search_and_replace",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_code_definition_names",
|
||||
"browser_action",
|
||||
"use_mcp_tool",
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"attempt_completion",
|
||||
"switch_mode",
|
||||
"new_task",
|
||||
"fetch_instructions",
|
||||
] as const
|
||||
|
||||
export type ToolName = (typeof toolNames)[number]
|
||||
|
||||
export const toolParamNames = [
|
||||
"command",
|
||||
"path",
|
||||
|
|
@ -167,14 +144,6 @@ export interface NewTaskToolUse extends ToolUse {
|
|||
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message">>
|
||||
}
|
||||
|
||||
export type ToolUsage = Record<
|
||||
ToolName,
|
||||
{
|
||||
attempts: number
|
||||
failures: number
|
||||
}
|
||||
>
|
||||
|
||||
// Define tool group configuration
|
||||
export type ToolGroupConfig = {
|
||||
tools: readonly string[]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue