Skip to content

Commit 16bd72a

Browse files
author
Your Name
committed
feat: implement condensing feedback from GitHub comments
1 parent fffa5d3 commit 16bd72a

File tree

5 files changed

+117
-60
lines changed

5 files changed

+117
-60
lines changed

src/core/condense/__tests__/index.test.ts

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ import { ApiHandler } from "../../../api"
33
import { ApiMessage } from "../../task-persistence/apiMessages"
44
import { maybeRemoveImageBlocks } from "../../../api/transform/image-cleaning"
55
import { summarizeConversation, getMessagesSinceLastSummary, N_MESSAGES_TO_KEEP } from "../index"
6+
import { telemetryService } from "../../../services/telemetry/TelemetryService"
67

78
// Mock dependencies
89
jest.mock("../../../api/transform/image-cleaning", () => ({
910
maybeRemoveImageBlocks: jest.fn((messages: ApiMessage[], _apiHandler: ApiHandler) => [...messages]),
1011
}))
1112

13+
jest.mock("../../../services/telemetry/TelemetryService", () => ({
14+
telemetryService: {
15+
captureContextCondensed: jest.fn(),
16+
},
17+
}))
18+
1219
const taskId = "test-task-id"
1320

1421
describe("getMessagesSinceLastSummary", () => {
@@ -302,6 +309,9 @@ describe("summarizeConversation with custom settings", () => {
302309
// Reset mocks
303310
jest.clearAllMocks()
304311

312+
// Reset telemetry mock
313+
;(telemetryService.captureContextCondensed as jest.Mock).mockClear()
314+
305315
// Setup mock API handlers
306316
mockMainApiHandler = {
307317
createMessage: jest.fn().mockImplementation(() => {
@@ -487,12 +497,7 @@ describe("summarizeConversation with custom settings", () => {
487497
/**
488498
* Test that telemetry is called for custom prompt usage
489499
*/
490-
it("should log when using custom prompt", async () => {
491-
// Mock console.log to verify logging
492-
const originalLog = console.log
493-
const mockLog = jest.fn()
494-
console.log = mockLog
495-
500+
it("should capture telemetry when using custom prompt", async () => {
496501
await summarizeConversation(
497502
sampleMessages,
498503
mockMainApiHandler,
@@ -502,24 +507,19 @@ describe("summarizeConversation with custom settings", () => {
502507
"Custom prompt",
503508
)
504509

505-
// Verify logging was called
506-
expect(mockLog).toHaveBeenCalledWith(
507-
expect.stringContaining(`Task [${taskId}]: Using custom condensing prompt.`),
510+
// Verify telemetry was called with custom prompt flag
511+
expect(telemetryService.captureContextCondensed).toHaveBeenCalledWith(
512+
taskId,
513+
false,
514+
true, // usedCustomPrompt
515+
false, // usedCustomApiHandler
508516
)
509-
510-
// Restore console.log
511-
console.log = originalLog
512517
})
513518

514519
/**
515520
* Test that telemetry is called for custom API handler usage
516521
*/
517-
it("should log when using custom API handler", async () => {
518-
// Mock console.log to verify logging
519-
const originalLog = console.log
520-
const mockLog = jest.fn()
521-
console.log = mockLog
522-
522+
it("should capture telemetry when using custom API handler", async () => {
523523
await summarizeConversation(
524524
sampleMessages,
525525
mockMainApiHandler,
@@ -530,12 +530,35 @@ describe("summarizeConversation with custom settings", () => {
530530
mockCondensingApiHandler,
531531
)
532532

533-
// Verify logging was called
534-
expect(mockLog).toHaveBeenCalledWith(
535-
expect.stringContaining(`Task [${taskId}]: Using custom API handler for condensing.`),
533+
// Verify telemetry was called with custom API handler flag
534+
expect(telemetryService.captureContextCondensed).toHaveBeenCalledWith(
535+
taskId,
536+
false,
537+
false, // usedCustomPrompt
538+
true, // usedCustomApiHandler
539+
)
540+
})
541+
542+
/**
543+
* Test that telemetry is called with both custom prompt and API handler
544+
*/
545+
it("should capture telemetry when using both custom prompt and API handler", async () => {
546+
await summarizeConversation(
547+
sampleMessages,
548+
mockMainApiHandler,
549+
defaultSystemPrompt,
550+
taskId,
551+
true, // isAutomaticTrigger
552+
"Custom prompt",
553+
mockCondensingApiHandler,
536554
)
537555

538-
// Restore console.log
539-
console.log = originalLog
556+
// Verify telemetry was called with both flags
557+
expect(telemetryService.captureContextCondensed).toHaveBeenCalledWith(
558+
taskId,
559+
true, // isAutomaticTrigger
560+
true, // usedCustomPrompt
561+
true, // usedCustomApiHandler
562+
)
540563
})
541564
})

src/core/condense/index.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,12 @@ export async function summarizeConversation(
8484
customCondensingPrompt?: string,
8585
condensingApiHandler?: ApiHandler,
8686
): Promise<SummarizeResponse> {
87-
telemetryService.captureContextCondensed(taskId, isAutomaticTrigger ?? false)
87+
telemetryService.captureContextCondensed(
88+
taskId,
89+
isAutomaticTrigger ?? false,
90+
!!customCondensingPrompt?.trim(),
91+
!!condensingApiHandler,
92+
)
8893
const response: SummarizeResponse = { messages, cost: 0, summary: "" }
8994
const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(0, -N_MESSAGES_TO_KEEP))
9095
if (messagesToSummarize.length <= 1) {
@@ -131,22 +136,6 @@ export async function summarizeConversation(
131136
}
132137
}
133138

134-
// Log when using custom prompt for debugging and telemetry
135-
if (customCondensingPrompt?.trim()) {
136-
console.log(`Task [${taskId}]: Using custom condensing prompt.`)
137-
// TODO: Add telemetry for custom condensing prompt usage
138-
// This would require extending the telemetry service with a new method like:
139-
// telemetryService.captureCustomCondensingPromptUsed(taskId);
140-
}
141-
142-
// Log when using custom API handler for condensing
143-
if (condensingApiHandler) {
144-
console.log(`Task [${taskId}]: Using custom API handler for condensing.`)
145-
// TODO: Add telemetry for custom condensing API handler usage
146-
// This would require extending the telemetry service with a new method like:
147-
// telemetryService.captureCustomCondensingApiUsed(taskId, condensingApiConfigId);
148-
}
149-
150139
const stream = handlerToUse.createMessage(promptToUse, requestMessages)
151140
let summary = ""
152141
let cost = 0

src/core/task/Task.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,20 +1493,20 @@ export class Task extends EventEmitter<ClineEvents> {
14931493
}
14941494

14951495
public async *attemptApiRequest(retryAttempt: number = 0): ApiStream {
1496+
const state = await this.providerRef.deref()?.getState()
14961497
const {
14971498
apiConfiguration,
14981499
autoApprovalEnabled,
14991500
alwaysApproveResubmit,
15001501
requestDelaySeconds,
15011502
experiments,
15021503
autoCondenseContextPercent = 100,
1503-
} = (await this.providerRef.deref()?.getState()) ?? {}
1504+
} = state ?? {}
15041505

15051506
// Get condensing configuration for automatic triggers
1506-
const state = await this.providerRef.deref()?.getState()
1507-
const customCondensingPrompt = state ? (state as any).customCondensingPrompt : undefined
1508-
const condensingApiConfigId = state ? (state as any).condensingApiConfigId : undefined
1509-
const listApiConfigMeta = state ? (state as any).listApiConfigMeta : undefined
1507+
const customCondensingPrompt = state?.customCondensingPrompt
1508+
const condensingApiConfigId = state?.condensingApiConfigId
1509+
const listApiConfigMeta = state?.listApiConfigMeta
15101510

15111511
// Determine API handler to use for condensing
15121512
let condensingApiHandler: ApiHandler | undefined
@@ -1563,7 +1563,7 @@ export class Task extends EventEmitter<ClineEvents> {
15631563

15641564
const contextWindow = modelInfo.contextWindow
15651565

1566-
const autoCondenseContext = experiments?.autoCondenseContext ?? false
1566+
const autoCondenseContext = state?.experiments?.autoCondenseContext ?? false
15671567
const truncateResult = await truncateConversationIfNeeded({
15681568
messages: this.apiConversationHistory,
15691569
totalTokens: contextTokens,
@@ -1602,8 +1602,7 @@ export class Task extends EventEmitter<ClineEvents> {
16021602
)
16031603

16041604
// Check if we've reached the maximum number of auto-approved requests
1605-
const { allowedMaxRequests } = (await this.providerRef.deref()?.getState()) ?? {}
1606-
const maxRequests = allowedMaxRequests || Infinity
1605+
const maxRequests = state?.allowedMaxRequests || Infinity
16071606

16081607
// Increment the counter for each new API request
16091608
this.consecutiveAutoApprovedRequestsCount++
@@ -1628,7 +1627,7 @@ export class Task extends EventEmitter<ClineEvents> {
16281627
} catch (error) {
16291628
this.isWaitingForFirstChunk = false
16301629
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
1631-
if (autoApprovalEnabled && alwaysApproveResubmit) {
1630+
if (state?.autoApprovalEnabled && state?.alwaysApproveResubmit) {
16321631
let errorMsg
16331632

16341633
if (error.error?.metadata?.raw) {
@@ -1639,7 +1638,7 @@ export class Task extends EventEmitter<ClineEvents> {
16391638
errorMsg = "Unknown error"
16401639
}
16411640

1642-
const baseDelay = requestDelaySeconds || 5
1641+
const baseDelay = state.requestDelaySeconds || 5
16431642
let exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt))
16441643

16451644
// If the error is a 429, and the error details contain a retry delay, use that delay instead of exponential backoff

src/services/telemetry/TelemetryService.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,18 @@ class TelemetryService {
120120
this.captureEvent(PostHogClient.EVENTS.TASK.CHECKPOINT_RESTORED, { taskId })
121121
}
122122

123-
public captureContextCondensed(taskId: string, isAutomaticTrigger: boolean): void {
124-
this.captureEvent(PostHogClient.EVENTS.TASK.CONTEXT_CONDENSED, { taskId, isAutomaticTrigger })
123+
public captureContextCondensed(
124+
taskId: string,
125+
isAutomaticTrigger: boolean,
126+
usedCustomPrompt?: boolean,
127+
usedCustomApiHandler?: boolean,
128+
): void {
129+
this.captureEvent(PostHogClient.EVENTS.TASK.CONTEXT_CONDENSED, {
130+
taskId,
131+
isAutomaticTrigger,
132+
...(usedCustomPrompt !== undefined && { usedCustomPrompt }),
133+
...(usedCustomApiHandler !== undefined && { usedCustomApiHandler }),
134+
})
125135
}
126136

127137
public captureSlidingWindowTruncation(taskId: string): void {

webview-ui/src/components/settings/ExperimentalSettings.tsx

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,46 @@ import { ExperimentalFeature } from "./ExperimentalFeature"
1414
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider } from "@/components/ui/"
1515
import { VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
1616

17+
const SUMMARY_PROMPT = `\
18+
Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
19+
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the conversation and supporting any continuing tasks.
20+
21+
Your summary should be structured as follows:
22+
Context: The context to continue the conversation with. If applicable based on the current task, this should include:
23+
1. Previous Conversation: High level details about what was discussed throughout the entire conversation with the user. This should be written to allow someone to be able to follow the general overarching conversation flow.
24+
2. Current Work: Describe in detail what was being worked on prior to this request to summarize the conversation. Pay special attention to the more recent messages in the conversation.
25+
3. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for continuing with this work.
26+
4. Relevant Files and Code: If applicable, enumerate specific files and code sections examined, modified, or created for the task continuation. Pay special attention to the most recent messages and changes.
27+
5. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts.
28+
6. Pending Tasks and Next Steps: Outline all pending tasks that you have explicitly been asked to work on, as well as list the next steps you will take for all outstanding work, if applicable. Include code snippets where they add clarity. For any next steps, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no information loss in context between tasks.
29+
30+
Example summary structure:
31+
1. Previous Conversation:
32+
[Detailed description]
33+
2. Current Work:
34+
[Detailed description]
35+
3. Key Technical Concepts:
36+
- [Concept 1]
37+
- [Concept 2]
38+
- [...]
39+
4. Relevant Files and Code:
40+
- [File Name 1]
41+
- [Summary of why this file is important]
42+
- [Summary of the changes made to this file, if any]
43+
- [Important Code Snippet]
44+
- [File Name 2]
45+
- [Important Code Snippet]
46+
- [...]
47+
5. Problem Solving:
48+
[Detailed description]
49+
6. Pending Tasks and Next Steps:
50+
- [Task 1 details & next steps]
51+
- [Task 2 details & next steps]
52+
- [...]
53+
54+
Output only the summary of the conversation so far, without any additional commentary or explanation.
55+
`
56+
1757
type ExperimentalSettingsProps = HTMLAttributes<HTMLDivElement> & {
1858
experiments: Record<ExperimentId, boolean>
1959
setExperimentEnabled: SetExperimentEnabled
@@ -140,7 +180,7 @@ export const ExperimentalSettings = ({
140180
</div>
141181
<VSCodeTextArea
142182
resize="vertical"
143-
value={customCondensingPrompt || ""}
183+
value={customCondensingPrompt || SUMMARY_PROMPT}
144184
onChange={(e) => {
145185
const value = (e.target as HTMLTextAreaElement).value
146186
setCustomCondensingPrompt(value)
@@ -149,26 +189,22 @@ export const ExperimentalSettings = ({
149189
text: value,
150190
})
151191
}}
152-
placeholder={t("settings:experimental.customCondensingPrompt.placeholder")}
153192
rows={8}
154193
className="w-full font-mono text-sm"
155194
/>
156-
<div className="mt-2 flex justify-between items-center">
195+
<div className="mt-2">
157196
<Button
158197
variant="secondary"
159198
size="sm"
160199
onClick={() => {
161-
setCustomCondensingPrompt("")
200+
setCustomCondensingPrompt(SUMMARY_PROMPT)
162201
vscode.postMessage({
163202
type: "updateCondensingPrompt",
164-
text: "",
203+
text: SUMMARY_PROMPT,
165204
})
166205
}}>
167206
{t("settings:experimental.customCondensingPrompt.reset")}
168207
</Button>
169-
<div className="text-xs text-vscode-descriptionForeground">
170-
{t("settings:experimental.customCondensingPrompt.hint")}
171-
</div>
172208
</div>
173209
</div>
174210
</div>

0 commit comments

Comments
 (0)