Skip to content

Commit c59edca

Browse files
committed
refactor: extract synthetic message functionality to separate module
- Moved shouldUseSyntheticMessages, handleFirstMessageWithFileMentions, and createSyntheticReadFileMessage to new synthetic-messages.ts module - Made addToApiConversationHistory public to allow access from the new module - Updated imports and function calls in Task.ts - Addresses PR feedback about Task.ts getting too large
1 parent 688c493 commit c59edca

File tree

2 files changed

+111
-100
lines changed

2 files changed

+111
-100
lines changed

src/core/task/Task.ts

Lines changed: 8 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-
7979
import { MultiFileSearchReplaceDiffStrategy } from "../diff/strategies/multi-file-search-replace"
8080
import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "../task-persistence"
8181
import { getEnvironmentDetails } from "../environment/getEnvironmentDetails"
82+
import {
83+
shouldUseSyntheticMessages,
84+
handleFirstMessageWithFileMentions,
85+
createSyntheticReadFileMessage,
86+
} from "./synthetic-messages"
8287
import {
8388
type CheckpointDiffOptions,
8489
type CheckpointRestoreOptions,
@@ -88,7 +93,6 @@ import {
8893
checkpointDiff,
8994
} from "../checkpoints"
9095
import { processUserContentMentions } from "../mentions/processUserContentMentions"
91-
import { extractFileMentions, hasFileMentions } from "../mentions/extractFileMentions"
9296
import { ApiMessage } from "../task-persistence/apiMessages"
9397
import { getMessagesSinceLastSummary, summarizeConversation } from "../condense"
9498
import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
@@ -330,7 +334,7 @@ export class Task extends EventEmitter<ClineEvents> {
330334
return readApiMessages({ taskId: this.taskId, globalStoragePath: this.globalStoragePath })
331335
}
332336

333-
private async addToApiConversationHistory(message: Anthropic.MessageParam) {
337+
public async addToApiConversationHistory(message: Anthropic.MessageParam) {
334338
const messageWithTs = { ...message, ts: Date.now() }
335339
this.apiConversationHistory.push(messageWithTs)
336340
await this.saveApiConversationHistory()
@@ -1120,102 +1124,6 @@ export class Task extends EventEmitter<ClineEvents> {
11201124

11211125
// Helper methods for synthetic message generation
11221126

1123-
/**
1124-
* Checks if we should use synthetic messages for the first user message.
1125-
* This is true when the message contains @filename mentions.
1126-
*/
1127-
private shouldUseSyntheticMessages(userContent: Anthropic.Messages.ContentBlockParam[]): boolean {
1128-
// Only check text blocks for file mentions
1129-
return hasFileMentions(userContent)
1130-
}
1131-
1132-
/**
1133-
* Handles the first message when it contains file mentions by creating synthetic messages.
1134-
* This implements the 3-message pattern:
1135-
* 1. Original user message (without embedded file content)
1136-
* 2. Synthetic assistant message with read_file tool calls
1137-
* 3. User message with file contents
1138-
*/
1139-
private async handleFirstMessageWithFileMentions(
1140-
userContent: Anthropic.Messages.ContentBlockParam[],
1141-
): Promise<void> {
1142-
// Extract file mentions from the user content
1143-
const fileMentions: string[] = []
1144-
for (const block of userContent) {
1145-
if (block.type === "text" && block.text) {
1146-
const mentions = extractFileMentions(block.text)
1147-
fileMentions.push(...mentions.map((m) => m.path))
1148-
}
1149-
}
1150-
1151-
if (fileMentions.length === 0) {
1152-
// No file mentions found, proceed normally
1153-
return
1154-
}
1155-
1156-
// Step 1: Add the original user message to conversation history (without processing mentions)
1157-
await this.addToApiConversationHistory({ role: "user", content: userContent })
1158-
1159-
// Step 2: Create synthetic assistant message with read_file tool calls
1160-
const syntheticAssistantContent = this.createSyntheticReadFileMessage(fileMentions)
1161-
await this.addToApiConversationHistory({
1162-
role: "assistant",
1163-
content: [{ type: "text", text: syntheticAssistantContent }],
1164-
})
1165-
1166-
// Step 3: Process the mentions to get file contents and create user response
1167-
const {
1168-
showRooIgnoredFiles = true,
1169-
includeDiagnosticMessages = true,
1170-
maxDiagnosticMessages = 50,
1171-
} = (await this.providerRef.deref()?.getState()) ?? {}
1172-
1173-
const processedContent = await processUserContentMentions({
1174-
userContent,
1175-
cwd: this.cwd,
1176-
urlContentFetcher: this.urlContentFetcher,
1177-
fileContextTracker: this.fileContextTracker,
1178-
rooIgnoreController: this.rooIgnoreController,
1179-
showRooIgnoredFiles,
1180-
includeDiagnosticMessages,
1181-
maxDiagnosticMessages,
1182-
})
1183-
1184-
// Add environment details
1185-
const environmentDetails = await getEnvironmentDetails(this, true)
1186-
const fileContentResponse = [...processedContent, { type: "text" as const, text: environmentDetails }]
1187-
1188-
// Add the file content as a user message
1189-
await this.addToApiConversationHistory({ role: "user", content: fileContentResponse })
1190-
1191-
// Now continue with the normal flow - the model will see all 3 messages
1192-
// but the task history only stored the original message without embedded content
1193-
}
1194-
1195-
/**
1196-
* Creates a synthetic assistant message with read_file tool calls
1197-
*/
1198-
private createSyntheticReadFileMessage(filePaths: string[]): string {
1199-
if (filePaths.length === 0) {
1200-
return ""
1201-
}
1202-
1203-
// Create read_file tool calls for each file
1204-
const toolCalls = filePaths
1205-
.map((path) => {
1206-
return `<read_file>
1207-
<args>
1208-
<file>
1209-
<path>${path}</path>
1210-
</file>
1211-
</args>
1212-
</read_file>`
1213-
})
1214-
.join("\n\n")
1215-
1216-
return `I'll help you with that. Let me first read the mentioned file${filePaths.length > 1 ? "s" : ""} to understand the context.\n\n${toolCalls}`
1217-
}
1218-
12191127
// Task Loop
12201128

12211129
private async initiateTaskLoop(userContent: Anthropic.Messages.ContentBlockParam[]): Promise<void> {
@@ -1230,9 +1138,9 @@ export class Task extends EventEmitter<ClineEvents> {
12301138

12311139
while (!this.abort) {
12321140
// Check if this is the first message and contains file mentions
1233-
if (isFirstMessage && this.shouldUseSyntheticMessages(nextUserContent)) {
1141+
if (isFirstMessage && shouldUseSyntheticMessages(nextUserContent)) {
12341142
// Handle first message with file mentions using synthetic messages
1235-
await this.handleFirstMessageWithFileMentions(nextUserContent)
1143+
await handleFirstMessageWithFileMentions(this, nextUserContent)
12361144
isFirstMessage = false
12371145
// The synthetic messages have been processed, continue with normal flow
12381146
// but skip the first iteration since we've already handled it
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { Anthropic } from "@anthropic-ai/sdk"
2+
import { extractFileMentions, hasFileMentions } from "../mentions/extractFileMentions"
3+
import { processUserContentMentions } from "../mentions/processUserContentMentions"
4+
import { getEnvironmentDetails } from "../environment/getEnvironmentDetails"
5+
import type { Task } from "./Task"
6+
7+
/**
8+
* Checks if synthetic messages should be used for the given user content.
9+
* This is true when the message contains @filename mentions.
10+
*/
11+
export function shouldUseSyntheticMessages(userContent: Anthropic.Messages.ContentBlockParam[]): boolean {
12+
// Only check text blocks for file mentions
13+
return hasFileMentions(userContent)
14+
}
15+
16+
/**
17+
* Handles the first message when it contains file mentions by creating synthetic messages.
18+
* This implements the 3-message pattern:
19+
* 1. Original user message (without embedded file content)
20+
* 2. Synthetic assistant message with read_file tool calls
21+
* 3. User message with file contents
22+
*/
23+
export async function handleFirstMessageWithFileMentions(
24+
task: Task,
25+
userContent: Anthropic.Messages.ContentBlockParam[],
26+
): Promise<void> {
27+
// Extract file mentions from the user content
28+
const fileMentions: string[] = []
29+
for (const block of userContent) {
30+
if (block.type === "text" && block.text) {
31+
const mentions = extractFileMentions(block.text)
32+
fileMentions.push(...mentions.map((m) => m.path))
33+
}
34+
}
35+
36+
if (fileMentions.length === 0) {
37+
// No file mentions found, proceed normally
38+
return
39+
}
40+
41+
// Step 1: Add the original user message to conversation history (without processing mentions)
42+
await task.addToApiConversationHistory({ role: "user", content: userContent })
43+
44+
// Step 2: Create synthetic assistant message with read_file tool calls
45+
const syntheticAssistantContent = createSyntheticReadFileMessage(fileMentions)
46+
await task.addToApiConversationHistory({
47+
role: "assistant",
48+
content: [{ type: "text", text: syntheticAssistantContent }],
49+
})
50+
51+
// Step 3: Process the mentions to get file contents and create user response
52+
const providerState = await task.providerRef.deref()?.getState()
53+
const {
54+
showRooIgnoredFiles = true,
55+
includeDiagnosticMessages = true,
56+
maxDiagnosticMessages = 50,
57+
} = providerState ?? {}
58+
59+
const processedContent = await processUserContentMentions({
60+
userContent,
61+
cwd: task.cwd,
62+
urlContentFetcher: task.urlContentFetcher,
63+
fileContextTracker: task.fileContextTracker,
64+
rooIgnoreController: task.rooIgnoreController,
65+
showRooIgnoredFiles,
66+
includeDiagnosticMessages,
67+
maxDiagnosticMessages,
68+
})
69+
70+
// Add environment details
71+
const environmentDetails = await getEnvironmentDetails(task, true)
72+
const fileContentResponse = [...processedContent, { type: "text" as const, text: environmentDetails }]
73+
74+
// Add the file content as a user message
75+
await task.addToApiConversationHistory({ role: "user", content: fileContentResponse })
76+
77+
// Now continue with the normal flow - the model will see all 3 messages
78+
// but the task history only stored the original message without embedded content
79+
}
80+
81+
/**
82+
* Creates a synthetic assistant message with read_file tool calls
83+
*/
84+
export function createSyntheticReadFileMessage(filePaths: string[]): string {
85+
if (filePaths.length === 0) {
86+
return ""
87+
}
88+
89+
// Create read_file tool calls for each file
90+
const toolCalls = filePaths
91+
.map((path) => {
92+
return `<read_file>
93+
<args>
94+
<file>
95+
<path>${path}</path>
96+
</file>
97+
</args>
98+
</read_file>`
99+
})
100+
.join("\n\n")
101+
102+
return `I'll help you with that. Let me first read the mentioned file${filePaths.length > 1 ? "s" : ""} to understand the context.\n\n${toolCalls}`
103+
}

0 commit comments

Comments
 (0)