mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix(cli): count subagent stats
This commit is contained in:
committed by
Imanol Maiztegui
parent
2a94a0cc11
commit
9e419b3e86
@@ -1,8 +1,9 @@
|
||||
import type { Argv } from "yargs"
|
||||
import type { MessageV2 } from "../../session/message-v2" // kilocode_change
|
||||
import { cmd } from "./cmd"
|
||||
import { Session } from "../../session"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { Database, isNull } from "../../storage" // kilocode_change - isNull for root session filter
|
||||
import { Database } from "../../storage"
|
||||
import { SessionTable } from "../../session/session.sql"
|
||||
import { Project } from "../../project"
|
||||
import { Instance } from "../../project/instance"
|
||||
@@ -89,10 +90,7 @@ async function getCurrentProject(): Promise<Project.Info> {
|
||||
}
|
||||
|
||||
async function getAllSessions(): Promise<Session.Info[]> {
|
||||
// kilocode_change start - exclude subagent (child) sessions; their cost is already propagated into the
|
||||
// parent's tool-wrapper assistant message, so summing both would double-count (#6321)
|
||||
const rows = Database.use((db) => db.select().from(SessionTable).where(isNull(SessionTable.parent_id)).all())
|
||||
// kilocode_change end
|
||||
const rows = Database.use((db) => db.select().from(SessionTable).all())
|
||||
return rows.map((row) => Session.fromRow(row))
|
||||
}
|
||||
|
||||
@@ -196,7 +194,16 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.info.role === "assistant") {
|
||||
sessionCost += message.info.cost || 0
|
||||
// kilocode_change start - count propagated subagent cost once but keep child model stats (#6321)
|
||||
const cost = (() => {
|
||||
const parts = message.parts.filter(
|
||||
(part: MessageV2.Part): part is MessageV2.StepFinishPart => part.type === "step-finish",
|
||||
)
|
||||
if (parts.length === 0) return message.info.cost || 0
|
||||
return parts.reduce((sum: number, part: MessageV2.StepFinishPart) => sum + part.cost, 0)
|
||||
})()
|
||||
if (!session.parentID) sessionCost += message.info.cost || 0
|
||||
// kilocode_change end
|
||||
|
||||
const modelKey = `${message.info.providerID}/${message.info.modelID}`
|
||||
if (!sessionModelUsage[modelKey]) {
|
||||
@@ -207,7 +214,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
}
|
||||
}
|
||||
sessionModelUsage[modelKey].messages++
|
||||
sessionModelUsage[modelKey].cost += message.info.cost || 0
|
||||
sessionModelUsage[modelKey].cost += cost
|
||||
|
||||
if (message.info.tokens) {
|
||||
sessionTokens.input += message.info.tokens.input || 0
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Verifies `kilo stats` does not double-count subagent cost. The task tool
|
||||
// propagates each child session's total cost up to the parent's tool-wrapper
|
||||
// assistant message. If stats summed every session indiscriminately, that
|
||||
// propagated cost would appear in both the parent wrapper and the child's
|
||||
// own messages. The aggregator is now filtered to root sessions (#6321).
|
||||
// Verifies `kilo stats` does not double-count subagent cost while still
|
||||
// including child-session messages, tokens, tools, and model usage. The task
|
||||
// tool propagates each child session's total cost up to the parent's
|
||||
// tool-wrapper assistant message (#6321).
|
||||
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { aggregateSessionStats } from "../../src/cli/cmd/stats"
|
||||
@@ -10,7 +9,7 @@ import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||
import { Session } from "../../src/session"
|
||||
import { MessageID } from "../../src/session/schema"
|
||||
import { MessageID, PartID } from "../../src/session/schema"
|
||||
import { Log } from "../../src/util"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
@@ -42,8 +41,40 @@ function assistant(sessionID: string, parentID: string, cost: number): MessageV2
|
||||
}
|
||||
}
|
||||
|
||||
async function step(sessionID: string, messageID: string, cost: number) {
|
||||
await Session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: messageID as any,
|
||||
sessionID: sessionID as any,
|
||||
type: "step-finish",
|
||||
reason: "stop",
|
||||
cost,
|
||||
tokens: { total: 15, input: 10, output: 5, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
})
|
||||
}
|
||||
|
||||
async function tool(sessionID: string, messageID: string) {
|
||||
const time = Date.now()
|
||||
await Session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: messageID as any,
|
||||
sessionID: sessionID as any,
|
||||
type: "tool",
|
||||
callID: "call_1",
|
||||
tool: "bash",
|
||||
state: {
|
||||
status: "completed",
|
||||
input: {},
|
||||
output: "ok",
|
||||
title: "bash",
|
||||
metadata: {},
|
||||
time: { start: time, end: time },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe("stats subagent cost", () => {
|
||||
test("totalCost excludes children whose cost was propagated into the parent", async () => {
|
||||
test("counts child usage without double-counting propagated cost", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
@@ -51,7 +82,6 @@ describe("stats subagent cost", () => {
|
||||
const parent = await Session.create({ title: "root" })
|
||||
const child = await Session.create({ parentID: parent.id, title: "subagent" })
|
||||
|
||||
// The parent's tool-wrapper assistant message shows the propagated total (own LLM + child).
|
||||
const userMsg = await Session.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
@@ -60,8 +90,9 @@ describe("stats subagent cost", () => {
|
||||
model: ref,
|
||||
time: { created: Date.now() },
|
||||
} as any)
|
||||
await Session.updateMessage(assistant(parent.id, userMsg.id, 1.5))
|
||||
// The child session independently records its own LLM cost.
|
||||
const parentMsg = await Session.updateMessage(assistant(parent.id, userMsg.id, 1.5))
|
||||
await step(parent.id, parentMsg.id, 1)
|
||||
|
||||
const childUser = await Session.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
@@ -70,14 +101,22 @@ describe("stats subagent cost", () => {
|
||||
model: ref,
|
||||
time: { created: Date.now() },
|
||||
} as any)
|
||||
await Session.updateMessage(assistant(child.id, childUser.id, 0.5))
|
||||
const childMsg = await Session.updateMessage(assistant(child.id, childUser.id, 0.5))
|
||||
await step(child.id, childMsg.id, 0.5)
|
||||
await tool(child.id, childMsg.id)
|
||||
|
||||
const stats = await aggregateSessionStats()
|
||||
// Without the fix, totalCost would be 1.5 + 0.5 = 2.0 (child counted twice).
|
||||
// With the fix, only the parent (root) session contributes: 1.5 (which already
|
||||
// includes the 0.5 propagated from the child).
|
||||
const model = stats.modelUsage["test/test-model"]!
|
||||
expect(stats.totalCost).toBeCloseTo(1.5, 6)
|
||||
expect(stats.totalSessions).toBe(1)
|
||||
expect(stats.totalSessions).toBe(2)
|
||||
expect(stats.totalMessages).toBe(4)
|
||||
expect(stats.totalTokens.input).toBe(20)
|
||||
expect(stats.totalTokens.output).toBe(10)
|
||||
expect(stats.toolUsage.bash).toBe(1)
|
||||
expect(model.messages).toBe(2)
|
||||
expect(model.tokens.input).toBe(20)
|
||||
expect(model.tokens.output).toBe(10)
|
||||
expect(model.cost).toBeCloseTo(1.5, 6)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user