Skip to content

Commit 4d762a1

Browse files
committed
Publish token usage metrics
1 parent 280b75d commit 4d762a1

File tree

8 files changed

+147
-72
lines changed

8 files changed

+147
-72
lines changed

packages/cloud/src/bridge/ExtensionChannel.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export class ExtensionChannel extends BaseChannel<
187187
{ from: RooCodeEventName.TaskUnpaused, to: ExtensionBridgeEventName.TaskUnpaused },
188188
{ from: RooCodeEventName.TaskSpawned, to: ExtensionBridgeEventName.TaskSpawned },
189189
{ from: RooCodeEventName.TaskUserMessage, to: ExtensionBridgeEventName.TaskUserMessage },
190+
{ from: RooCodeEventName.TaskTokenUsageUpdated, to: ExtensionBridgeEventName.TaskTokenUsageUpdated },
190191
] as const
191192

192193
eventMapping.forEach(({ from, to }) => {
@@ -229,11 +230,12 @@ export class ExtensionChannel extends BaseChannel<
229230
task: task
230231
? {
231232
taskId: task.taskId,
233+
parentTaskId: task.parentTaskId,
234+
childTaskId: task.childTaskId,
232235
taskStatus: task.taskStatus,
233236
taskAsk: task?.taskAsk,
234237
queuedMessages: task.queuedMessages,
235-
parentTaskId: task.parentTaskId,
236-
childTaskId: task.childTaskId,
238+
tokenUsage: task.tokenUsage,
237239
...task.metadata,
238240
}
239241
: { taskId: "", taskStatus: TaskStatus.None },

packages/types/npm/package.metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@roo-code/types",
3-
"version": "1.71.0",
3+
"version": "1.72.0",
44
"description": "TypeScript type definitions for Roo Code.",
55
"publishConfig": {
66
"access": "public",

packages/types/src/cloud.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { TaskStatus, taskMetadataSchema } from "./task.js"
77
import { globalSettingsSchema } from "./global-settings.js"
88
import { providerSettingsWithIdSchema } from "./provider-settings.js"
99
import { mcpMarketplaceItemSchema } from "./marketplace.js"
10-
import { clineMessageSchema, queuedMessageSchema } from "./message.js"
10+
import { clineMessageSchema, queuedMessageSchema, tokenUsageSchema } from "./message.js"
1111
import { staticAppPropertiesSchema, gitPropertiesSchema } from "./telemetry.js"
1212

1313
/**
@@ -363,6 +363,7 @@ const extensionTaskSchema = z.object({
363363
queuedMessages: z.array(queuedMessageSchema).optional(),
364364
parentTaskId: z.string().optional(),
365365
childTaskId: z.string().optional(),
366+
tokenUsage: tokenUsageSchema.optional(),
366367
...taskMetadataSchema.shape,
367368
})
368369

@@ -412,6 +413,8 @@ export enum ExtensionBridgeEventName {
412413

413414
TaskUserMessage = RooCodeEventName.TaskUserMessage,
414415

416+
TaskTokenUsageUpdated = RooCodeEventName.TaskTokenUsageUpdated,
417+
415418
ModeChanged = RooCodeEventName.ModeChanged,
416419
ProviderProfileChanged = RooCodeEventName.ProviderProfileChanged,
417420

@@ -494,6 +497,12 @@ export const extensionBridgeEventSchema = z.discriminatedUnion("type", [
494497
timestamp: z.number(),
495498
}),
496499

500+
z.object({
501+
type: z.literal(ExtensionBridgeEventName.TaskTokenUsageUpdated),
502+
instance: extensionInstanceSchema,
503+
timestamp: z.number(),
504+
}),
505+
497506
z.object({
498507
type: z.literal(ExtensionBridgeEventName.ModeChanged),
499508
instance: extensionInstanceSchema,

packages/types/src/task.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export type TaskProviderEvents = {
7575

7676
[RooCodeEventName.TaskUserMessage]: [taskId: string]
7777

78+
[RooCodeEventName.TaskTokenUsageUpdated]: [taskId: string, tokenUsage: TokenUsage]
79+
7880
[RooCodeEventName.ModeChanged]: [mode: string]
7981
[RooCodeEventName.ProviderProfileChanged]: [config: { name: string; provider?: string }]
8082
}
@@ -116,6 +118,7 @@ export interface TaskLike {
116118
readonly taskStatus: TaskStatus
117119
readonly taskAsk: ClineMessage | undefined
118120
readonly queuedMessages: QueuedMessage[]
121+
readonly tokenUsage: TokenUsage | undefined
119122

120123
on<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this
121124
off<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this

src/core/checkpoints/index.ts

Lines changed: 64 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,18 @@ import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider
1717
import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints"
1818

1919
export async function getCheckpointService(
20-
cline: Task,
20+
task: Task,
2121
{ interval = 250, timeout = 15_000 }: { interval?: number; timeout?: number } = {},
2222
) {
23-
if (!cline.enableCheckpoints) {
23+
if (!task.enableCheckpoints) {
2424
return undefined
2525
}
26-
if (cline.checkpointService) {
27-
return cline.checkpointService
26+
27+
if (task.checkpointService) {
28+
return task.checkpointService
2829
}
2930

30-
const provider = cline.providerRef.deref()
31+
const provider = task.providerRef.deref()
3132

3233
const log = (message: string) => {
3334
console.log(message)
@@ -42,60 +43,63 @@ export async function getCheckpointService(
4243
console.log("[Task#getCheckpointService] initializing checkpoints service")
4344

4445
try {
45-
const workspaceDir = cline.cwd || getWorkspacePath()
46+
const workspaceDir = task.cwd || getWorkspacePath()
4647

4748
if (!workspaceDir) {
4849
log("[Task#getCheckpointService] workspace folder not found, disabling checkpoints")
49-
cline.enableCheckpoints = false
50+
task.enableCheckpoints = false
5051
return undefined
5152
}
5253

5354
const globalStorageDir = provider?.context.globalStorageUri.fsPath
5455

5556
if (!globalStorageDir) {
5657
log("[Task#getCheckpointService] globalStorageDir not found, disabling checkpoints")
57-
cline.enableCheckpoints = false
58+
task.enableCheckpoints = false
5859
return undefined
5960
}
6061

6162
const options: CheckpointServiceOptions = {
62-
taskId: cline.taskId,
63+
taskId: task.taskId,
6364
workspaceDir,
6465
shadowDir: globalStorageDir,
6566
log,
6667
}
67-
if (cline.checkpointServiceInitializing) {
68+
69+
if (task.checkpointServiceInitializing) {
6870
await pWaitFor(
6971
() => {
7072
console.log("[Task#getCheckpointService] waiting for service to initialize")
71-
return !!cline.checkpointService && !!cline?.checkpointService?.isInitialized
73+
return !!task.checkpointService && !!task?.checkpointService?.isInitialized
7274
},
7375
{ interval, timeout },
7476
)
75-
if (!cline?.checkpointService) {
76-
cline.enableCheckpoints = false
77+
if (!task?.checkpointService) {
78+
task.enableCheckpoints = false
7779
return undefined
7880
}
79-
return cline.checkpointService
81+
return task.checkpointService
8082
}
81-
if (!cline.enableCheckpoints) {
83+
84+
if (!task.enableCheckpoints) {
8285
return undefined
8386
}
87+
8488
const service = RepoPerTaskCheckpointService.create(options)
85-
cline.checkpointServiceInitializing = true
86-
await checkGitInstallation(cline, service, log, provider)
87-
cline.checkpointService = service
89+
task.checkpointServiceInitializing = true
90+
await checkGitInstallation(task, service, log, provider)
91+
task.checkpointService = service
8892
return service
8993
} catch (err) {
9094
log(`[Task#getCheckpointService] ${err.message}`)
91-
cline.enableCheckpoints = false
92-
cline.checkpointServiceInitializing = false
95+
task.enableCheckpoints = false
96+
task.checkpointServiceInitializing = false
9397
return undefined
9498
}
9599
}
96100

97101
async function checkGitInstallation(
98-
cline: Task,
102+
task: Task,
99103
service: RepoPerTaskCheckpointService,
100104
log: (message: string) => void,
101105
provider: any,
@@ -105,8 +109,8 @@ async function checkGitInstallation(
105109

106110
if (!gitInstalled) {
107111
log("[Task#getCheckpointService] Git is not installed, disabling checkpoints")
108-
cline.enableCheckpoints = false
109-
cline.checkpointServiceInitializing = false
112+
task.enableCheckpoints = false
113+
task.checkpointServiceInitializing = false
110114

111115
// Show user-friendly notification
112116
const selection = await vscode.window.showWarningMessage(
@@ -124,56 +128,55 @@ async function checkGitInstallation(
124128
// Git is installed, proceed with initialization
125129
service.on("initialize", () => {
126130
log("[Task#getCheckpointService] service initialized")
127-
cline.checkpointServiceInitializing = false
131+
task.checkpointServiceInitializing = false
128132
})
129133

130134
service.on("checkpoint", ({ fromHash: from, toHash: to }) => {
131135
try {
132136
provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: to })
133137

134-
cline
135-
.say("checkpoint_saved", to, undefined, undefined, { from, to }, undefined, {
136-
isNonInteractive: true,
137-
})
138-
.catch((err) => {
139-
log("[Task#getCheckpointService] caught unexpected error in say('checkpoint_saved')")
140-
console.error(err)
141-
})
138+
task.say("checkpoint_saved", to, undefined, undefined, { from, to }, undefined, {
139+
isNonInteractive: true,
140+
}).catch((err) => {
141+
log("[Task#getCheckpointService] caught unexpected error in say('checkpoint_saved')")
142+
console.error(err)
143+
})
142144
} catch (err) {
143145
log("[Task#getCheckpointService] caught unexpected error in on('checkpoint'), disabling checkpoints")
144146
console.error(err)
145-
cline.enableCheckpoints = false
147+
task.enableCheckpoints = false
146148
}
147149
})
148150

149151
log("[Task#getCheckpointService] initializing shadow git")
152+
150153
try {
151154
await service.initShadowGit()
152155
} catch (err) {
153156
log(`[Task#getCheckpointService] initShadowGit -> ${err.message}`)
154-
cline.enableCheckpoints = false
157+
task.enableCheckpoints = false
155158
}
156159
} catch (err) {
157160
log(`[Task#getCheckpointService] Unexpected error during Git check: ${err.message}`)
158161
console.error("Git check error:", err)
159-
cline.enableCheckpoints = false
160-
cline.checkpointServiceInitializing = false
162+
task.enableCheckpoints = false
163+
task.checkpointServiceInitializing = false
161164
}
162165
}
163166

164-
export async function checkpointSave(cline: Task, force = false) {
165-
const service = await getCheckpointService(cline)
167+
export async function checkpointSave(task: Task, force = false) {
168+
const service = await getCheckpointService(task)
166169

167170
if (!service) {
168171
return
169172
}
170173

171-
TelemetryService.instance.captureCheckpointCreated(cline.taskId)
174+
TelemetryService.instance.captureCheckpointCreated(task.taskId)
172175

173176
// Start the checkpoint process in the background.
174-
return service.saveCheckpoint(`Task: ${cline.taskId}, Time: ${Date.now()}`, { allowEmpty: force }).catch((err) => {
177+
return service.saveCheckpoint(`Task: ${task.taskId}, Time: ${Date.now()}`, { allowEmpty: force }).catch((err) => {
175178
console.error("[Task#checkpointSave] caught unexpected error, disabling checkpoints", err)
176-
cline.enableCheckpoints = false
179+
task.enableCheckpoints = false
177180
})
178181
}
179182

@@ -183,39 +186,39 @@ export type CheckpointRestoreOptions = {
183186
mode: "preview" | "restore"
184187
}
185188

186-
export async function checkpointRestore(cline: Task, { ts, commitHash, mode }: CheckpointRestoreOptions) {
187-
const service = await getCheckpointService(cline)
189+
export async function checkpointRestore(task: Task, { ts, commitHash, mode }: CheckpointRestoreOptions) {
190+
const service = await getCheckpointService(task)
188191

189192
if (!service) {
190193
return
191194
}
192195

193-
const index = cline.clineMessages.findIndex((m) => m.ts === ts)
196+
const index = task.clineMessages.findIndex((m) => m.ts === ts)
194197

195198
if (index === -1) {
196199
return
197200
}
198201

199-
const provider = cline.providerRef.deref()
202+
const provider = task.providerRef.deref()
200203

201204
try {
202205
await service.restoreCheckpoint(commitHash)
203-
TelemetryService.instance.captureCheckpointRestored(cline.taskId)
206+
TelemetryService.instance.captureCheckpointRestored(task.taskId)
204207
await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
205208

206209
if (mode === "restore") {
207-
await cline.overwriteApiConversationHistory(cline.apiConversationHistory.filter((m) => !m.ts || m.ts < ts))
210+
await task.overwriteApiConversationHistory(task.apiConversationHistory.filter((m) => !m.ts || m.ts < ts))
208211

209-
const deletedMessages = cline.clineMessages.slice(index + 1)
212+
const deletedMessages = task.clineMessages.slice(index + 1)
210213

211214
const { totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, totalCost } = getApiMetrics(
212-
cline.combineMessages(deletedMessages),
215+
task.combineMessages(deletedMessages),
213216
)
214217

215-
await cline.overwriteClineMessages(cline.clineMessages.slice(0, index + 1))
218+
await task.overwriteClineMessages(task.clineMessages.slice(0, index + 1))
216219

217220
// TODO: Verify that this is working as expected.
218-
await cline.say(
221+
await task.say(
219222
"api_req_deleted",
220223
JSON.stringify({
221224
tokensIn: totalTokensIn,
@@ -230,17 +233,17 @@ export async function checkpointRestore(cline: Task, { ts, commitHash, mode }: C
230233
// The task is already cancelled by the provider beforehand, but we
231234
// need to re-init to get the updated messages.
232235
//
233-
// This was take from Cline's implementation of the checkpoints
234-
// feature. The cline instance will hang if we don't cancel twice,
236+
// This was taken from Cline's implementation of the checkpoints
237+
// feature. The task instance will hang if we don't cancel twice,
235238
// so this is currently necessary, but it seems like a complicated
236239
// and hacky solution to a problem that I don't fully understand.
237240
// I'd like to revisit this in the future and try to improve the
238241
// task flow and the communication between the webview and the
239-
// Cline instance.
242+
// `Task` instance.
240243
provider?.cancelTask()
241244
} catch (err) {
242245
provider?.log("[checkpointRestore] disabling checkpoints for this task")
243-
cline.enableCheckpoints = false
246+
task.enableCheckpoints = false
244247
}
245248
}
246249

@@ -251,20 +254,21 @@ export type CheckpointDiffOptions = {
251254
mode: "full" | "checkpoint"
252255
}
253256

254-
export async function checkpointDiff(cline: Task, { ts, previousCommitHash, commitHash, mode }: CheckpointDiffOptions) {
255-
const service = await getCheckpointService(cline)
257+
export async function checkpointDiff(task: Task, { ts, previousCommitHash, commitHash, mode }: CheckpointDiffOptions) {
258+
const service = await getCheckpointService(task)
256259

257260
if (!service) {
258261
return
259262
}
260263

261-
TelemetryService.instance.captureCheckpointDiffed(cline.taskId)
264+
TelemetryService.instance.captureCheckpointDiffed(task.taskId)
262265

263266
let prevHash = commitHash
264267
let nextHash: string | undefined
265268

266269
const checkpoints = typeof service.getCheckpoints === "function" ? service.getCheckpoints() : []
267270
const idx = checkpoints.indexOf(commitHash)
271+
268272
if (idx !== -1 && idx < checkpoints.length - 1) {
269273
nextHash = checkpoints[idx + 1]
270274
} else {
@@ -293,8 +297,8 @@ export async function checkpointDiff(cline: Task, { ts, previousCommitHash, comm
293297
]),
294298
)
295299
} catch (err) {
296-
const provider = cline.providerRef.deref()
300+
const provider = task.providerRef.deref()
297301
provider?.log("[checkpointDiff] disabling checkpoints for this task")
298-
cline.enableCheckpoints = false
302+
task.enableCheckpoints = false
299303
}
300304
}

0 commit comments

Comments
 (0)