mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge pull request #10198 from Kilo-Org/fix/cli-track-review-telemetry
fix(cli): track /review completions in telemetry
This commit is contained in:
@@ -156,7 +156,7 @@ export namespace Telemetry {
|
||||
taskId?: string
|
||||
mode?: "review"
|
||||
feature?: "code_reviews"
|
||||
command?: "local-review" | "local-review-uncommitted"
|
||||
command?: "review" | "local-review" | "local-review-uncommitted"
|
||||
apiProvider: string
|
||||
modelId: string
|
||||
inputTokens?: number
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
export type ReviewTelemetry = {
|
||||
mode: "review"
|
||||
feature: "code_reviews"
|
||||
command: "local-review" | "local-review-uncommitted"
|
||||
command: "review" | "local-review" | "local-review-uncommitted"
|
||||
}
|
||||
|
||||
export namespace KiloSessionProcessor {
|
||||
@@ -22,12 +22,31 @@ export namespace KiloSessionProcessor {
|
||||
export const PROVIDER_FINISH_ERROR_MESSAGE =
|
||||
"The provider ended the response with an error before returning details. Start a new message to retry; Kilo will compact the oversized conversation first if needed."
|
||||
|
||||
export function reviewTelemetry(command: string): ReviewTelemetry | undefined {
|
||||
if (command === "local-review" || command === "local-review-uncommitted") {
|
||||
export function reviewTelemetry(command: string | undefined): ReviewTelemetry | undefined {
|
||||
if (command === "review" || command === "local-review" || command === "local-review-uncommitted") {
|
||||
return { mode: "review", feature: "code_reviews", command }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag the text parts of a prompt with review telemetry metadata so that
|
||||
* downstream LLM completions in the same turn (including child sessions
|
||||
* spawned by subtask commands) are attributed to the originating review
|
||||
* command. No-op when the command is not a recognized review command.
|
||||
*/
|
||||
export function markReviewTelemetry(
|
||||
parts: Array<{ type: string; metadata?: Record<string, unknown> }>,
|
||||
command: string | undefined,
|
||||
): ReviewTelemetry | undefined {
|
||||
const tel = reviewTelemetry(command)
|
||||
if (!tel) return
|
||||
for (const part of parts) {
|
||||
if (part.type !== "text") continue
|
||||
part.metadata = { ...part.metadata, ...tel }
|
||||
}
|
||||
return tel
|
||||
}
|
||||
|
||||
export function extractReviewTelemetry(parts: MessageV2.Part[]): ReviewTelemetry | undefined {
|
||||
for (const part of parts) {
|
||||
if (part.type !== "text") continue
|
||||
@@ -35,9 +54,8 @@ export namespace KiloSessionProcessor {
|
||||
if (!meta) continue
|
||||
if (meta.mode !== "review") continue
|
||||
if (meta.feature !== "code_reviews") continue
|
||||
const command = meta.command
|
||||
if (command !== "local-review" && command !== "local-review-uncommitted") continue
|
||||
return { mode: "review", feature: "code_reviews", command }
|
||||
const tel = reviewTelemetry(typeof meta.command === "string" ? meta.command : undefined)
|
||||
if (tel) return tel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1824,15 +1824,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
}
|
||||
|
||||
const templateParts = yield* resolvePromptParts(template)
|
||||
// kilocode_change start - mark local review commands for completion telemetry
|
||||
const telemetry = KiloSessionProcessor.reviewTelemetry(input.command)
|
||||
if (telemetry) {
|
||||
for (const part of templateParts) {
|
||||
if (part.type !== "text") continue
|
||||
part.metadata = { ...part.metadata, ...telemetry }
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
KiloSessionProcessor.markReviewTelemetry(templateParts, input.command) // kilocode_change - mark review commands for completion telemetry
|
||||
const isSubtask = (agent.mode === "subagent" && cmd.subtask !== false) || cmd.subtask === true
|
||||
const parts = isSubtask
|
||||
? [
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { SessionPrompt } from "../session/prompt"
|
||||
import { Config } from "@/config/config"
|
||||
import { KiloTask } from "../kilocode/tool/task" // kilocode_change
|
||||
import { KiloCostPropagation } from "../kilocode/session/cost-propagation" // kilocode_change
|
||||
import { KiloSessionProcessor } from "../kilocode/session/processor" // kilocode_change
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
export interface TaskPromptOps {
|
||||
@@ -154,6 +155,7 @@ export const TaskTool = Tool.define(
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const parts = yield* ops.resolvePromptParts(params.prompt)
|
||||
KiloSessionProcessor.markReviewTelemetry(parts, params.command) // kilocode_change - carry review command into child session telemetry
|
||||
const result = yield* ops.prompt({
|
||||
messageID,
|
||||
sessionID: nextSession.id,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// kilocode_change - new file
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { KiloSessionProcessor } from "../../src/kilocode/session/processor"
|
||||
import type { MessageV2 } from "../../src/session/message-v2"
|
||||
|
||||
const REVIEW_COMMANDS = ["review", "local-review", "local-review-uncommitted"] as const
|
||||
|
||||
const expected = (command: (typeof REVIEW_COMMANDS)[number]) => ({
|
||||
mode: "review" as const,
|
||||
feature: "code_reviews" as const,
|
||||
command,
|
||||
})
|
||||
|
||||
describe("KiloSessionProcessor.reviewTelemetry", () => {
|
||||
for (const command of REVIEW_COMMANDS) {
|
||||
test(`returns telemetry for ${command}`, () => {
|
||||
expect(KiloSessionProcessor.reviewTelemetry(command)).toEqual(expected(command))
|
||||
})
|
||||
}
|
||||
|
||||
test("returns undefined for an unrelated command", () => {
|
||||
expect(KiloSessionProcessor.reviewTelemetry("init")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("returns undefined for an undefined command", () => {
|
||||
expect(KiloSessionProcessor.reviewTelemetry(undefined)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("KiloSessionProcessor.markReviewTelemetry", () => {
|
||||
for (const command of REVIEW_COMMANDS) {
|
||||
test(`stamps text parts with telemetry for ${command}`, () => {
|
||||
const parts: Array<{ type: string; metadata?: Record<string, unknown> }> = [
|
||||
{ type: "text", metadata: { existing: "keep" } },
|
||||
{ type: "file" },
|
||||
{ type: "text" },
|
||||
]
|
||||
const tel = KiloSessionProcessor.markReviewTelemetry(parts, command)
|
||||
expect(tel).toEqual(expected(command))
|
||||
expect(parts[0].metadata).toEqual({ existing: "keep", ...expected(command) })
|
||||
expect(parts[1].metadata).toBeUndefined()
|
||||
expect(parts[2].metadata).toEqual({ ...expected(command) })
|
||||
})
|
||||
}
|
||||
|
||||
test("does nothing for an unrelated command", () => {
|
||||
const parts: Array<{ type: string; metadata?: Record<string, unknown> }> = [{ type: "text" }]
|
||||
expect(KiloSessionProcessor.markReviewTelemetry(parts, "init")).toBeUndefined()
|
||||
expect(parts[0].metadata).toBeUndefined()
|
||||
})
|
||||
|
||||
test("does nothing for an undefined command", () => {
|
||||
const parts: Array<{ type: string; metadata?: Record<string, unknown> }> = [{ type: "text" }]
|
||||
expect(KiloSessionProcessor.markReviewTelemetry(parts, undefined)).toBeUndefined()
|
||||
expect(parts[0].metadata).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("KiloSessionProcessor.extractReviewTelemetry", () => {
|
||||
for (const command of REVIEW_COMMANDS) {
|
||||
test(`recovers ${command} telemetry from marked text parts`, () => {
|
||||
const parts: Array<{ type: string; metadata?: Record<string, unknown> }> = [{ type: "text" }]
|
||||
KiloSessionProcessor.markReviewTelemetry(parts, command)
|
||||
const round = KiloSessionProcessor.extractReviewTelemetry(parts as unknown as MessageV2.Part[])
|
||||
expect(round).toEqual(expected(command))
|
||||
})
|
||||
}
|
||||
|
||||
test("returns undefined when parts have no review metadata", () => {
|
||||
const parts: Array<{ type: string; metadata?: Record<string, unknown> }> = [
|
||||
{ type: "text" },
|
||||
{ type: "text", metadata: { foo: "bar" } },
|
||||
]
|
||||
expect(KiloSessionProcessor.extractReviewTelemetry(parts as unknown as MessageV2.Part[])).toBeUndefined()
|
||||
})
|
||||
|
||||
test("returns undefined when command in metadata is unknown", () => {
|
||||
const parts: Array<{ type: string; metadata?: Record<string, unknown> }> = [
|
||||
{ type: "text", metadata: { mode: "review", feature: "code_reviews", command: "unknown" } },
|
||||
]
|
||||
expect(KiloSessionProcessor.extractReviewTelemetry(parts as unknown as MessageV2.Part[])).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { expect } from "bun:test"
|
||||
import { afterEach, expect, mock, spyOn } from "bun:test" // kilocode_change - spy on review telemetry
|
||||
import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change - assert review command telemetry
|
||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
@@ -216,6 +217,12 @@ function makeHttp() {
|
||||
const it = testEffect(makeHttp())
|
||||
const unix = process.platform !== "win32" ? it.live : it.live.skip
|
||||
|
||||
// kilocode_change start - restore any spies between tests so review telemetry spy never leaks
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
// Config that registers a custom "test" provider with a "test-model" model
|
||||
// so provider model lookup succeeds inside the loop.
|
||||
const cfg = {
|
||||
@@ -1985,6 +1992,41 @@ it.live("applies agent variant only when using agent model", () =>
|
||||
),
|
||||
)
|
||||
|
||||
// kilocode_change start - /review subtask path tags child completions for telemetry
|
||||
it.live(
|
||||
"review command marks child completions with review telemetry",
|
||||
() =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* ({ llm }) {
|
||||
const trackSpy = spyOn(Telemetry, "trackLlmCompletion")
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const chat = yield* sessions.create({
|
||||
title: "Review telemetry",
|
||||
permission: [{ permission: "*", pattern: "*", action: "allow" }],
|
||||
})
|
||||
|
||||
// child subagent's first LLM step needs non-zero usage so trackStep fires
|
||||
yield* llm.text("review done", { usage: { input: 100, output: 50 } })
|
||||
|
||||
yield* prompt.command({
|
||||
sessionID: chat.id,
|
||||
command: "review",
|
||||
arguments: "",
|
||||
agent: "general",
|
||||
})
|
||||
|
||||
const tagged = trackSpy.mock.calls
|
||||
.map((args) => args[0] as Parameters<typeof Telemetry.trackLlmCompletion>[0])
|
||||
.find((p) => p.mode === "review" && p.feature === "code_reviews" && p.command === "review")
|
||||
expect(tagged).toBeDefined()
|
||||
}),
|
||||
{ git: true, config: providerCfg },
|
||||
),
|
||||
30_000,
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
// Agent / command resolution errors
|
||||
|
||||
it.live(
|
||||
|
||||
Reference in New Issue
Block a user