Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cbedb2e
Remove redundant console.log in Cline.log method and add log entry tests
StevenTCramer May 4, 2025
7e6d8bc
Add log entry feature implementation and documentation
StevenTCramer May 4, 2025
2cd60eb
Move log entry documentation to its own section
StevenTCramer May 4, 2025
922e905
Fix log entry level issue by only logging complete entries. Added com…
StevenTCramer May 4, 2025
e2849eb
refactor(parse-assistant-message): break down large function into sma…
StevenTCramer May 5, 2025
dd0819b
Rename LogEntry to LogDirective
StevenTCramer May 5, 2025
f7780c8
refactor: extract LogDirective to its own file
StevenTCramer May 5, 2025
dba06d8
refactor: remove unused logLevels import from tools.ts
StevenTCramer May 5, 2025
ce3e282
Extract logging functionality from Cline.ts into dedicated LogManager…
StevenTCramer May 5, 2025
f05a372
Extract LogEntryParams to dedicated file
StevenTCramer May 5, 2025
ea5f180
Add changeset for structured logging system
StevenTCramer May 5, 2025
b195811
Fix lint error: Remove unused logLevels import in LogManager.test.ts
StevenTCramer May 5, 2025
d856b95
Rename log_entry to log_message for better clarity and consistency
StevenTCramer May 6, 2025
fb4b6fc
Rename log_entry to log_message for better clarity and consistency - …
StevenTCramer May 6, 2025
4af6732
Merge branch 'main' into Cramer/2025-05-04/Logging
StevenTCramer May 6, 2025
0a6f77b
Update system prompt snapshots to reflect log_entry to log_message te…
StevenTCramer May 6, 2025
3b29790
Merge branch 'main' into Cramer/2025-05-04/Logging
StevenTCramer May 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/healthy-fans-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": minor
---

Added structured logging system with schema validation for log entries, allowing the AI assistant to output diagnostic information to the VSCode output channel via <log_entry> blocks without requiring user approval
10 changes: 9 additions & 1 deletion src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import { validateToolUse } from "./mode-validator"
import { MultiSearchReplaceDiffStrategy } from "./diff/strategies/multi-search-replace"
import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "./task-persistence"
import { getEnvironmentDetails } from "./environment/getEnvironmentDetails"
import { LogManager } from "./logging"

type UserContent = Array<Anthropic.Messages.ContentBlockParam>

Expand Down Expand Up @@ -151,6 +152,7 @@ export class Cline extends EventEmitter<ClineEvents> {
diffStrategy?: DiffStrategy
diffEnabled: boolean = false
fuzzyMatchThreshold: number
private logManager: LogManager

apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
clineMessages: ClineMessage[] = []
Expand Down Expand Up @@ -232,7 +234,7 @@ export class Cline extends EventEmitter<ClineEvents> {

this.rooIgnoreController = new RooIgnoreController(this.cwd)
this.fileContextTracker = new FileContextTracker(provider, this.taskId)

this.logManager = new LogManager(provider)
this.rooIgnoreController.initialize().catch((error) => {
console.error("Failed to initialize RooIgnoreController:", error)
})
Expand Down Expand Up @@ -1621,6 +1623,12 @@ export class Cline extends EventEmitter<ClineEvents> {
const block = cloneDeep(this.assistantMessageContent[this.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too

switch (block.type) {
case "log_message": {
// Log messages are processed immediately without requiring approval
// and don't count as a tool use
this.logManager.processLogEntry(block.message, block.level, block.partial)
break
}
case "text": {
if (this.didRejectTool || this.didAlreadyUseTool) {
break
Expand Down
168 changes: 168 additions & 0 deletions src/core/assistant-message/__tests__/log-message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { parseAssistantMessage } from "../parse-assistant-message"

describe("Log Entry Parsing", () => {
it("should parse complete log entries correctly", () => {
const message = `<log_message>
<message>This is a test log message</message>
<level>debug</level>
</log_message>`

const result = parseAssistantMessage(message)

// Filter out empty text blocks
const filteredResult = result.filter((block) => !(block.type === "text" && block.content === ""))

expect(filteredResult).toHaveLength(1)
expect(filteredResult[0]).toEqual({
type: "log_message",
message: "This is a test log message",
level: "debug",
partial: false,
})
})

it("should mark partial log entries as partial", () => {
const message = `<log_message>
<message>This is a test log message</message>`

const result = parseAssistantMessage(message)

// Filter out empty text blocks
const filteredResult = result.filter((block) => !(block.type === "text" && block.content === ""))

expect(filteredResult).toHaveLength(1)
expect(filteredResult[0]).toEqual({
type: "log_message",
message: "This is a test log message",
level: "info", // Default level
partial: true,
})
})

it("should handle log entries with only message tag", () => {
const message = `<log_message>
<message>This is a test log message</message>
</log_message>`

const result = parseAssistantMessage(message)

// Filter out empty text blocks
const filteredResult = result.filter((block) => !(block.type === "text" && block.content === ""))

expect(filteredResult).toHaveLength(1)
expect(filteredResult[0]).toEqual({
type: "log_message",
message: "This is a test log message",
level: "info", // Default level
partial: false,
})
})

it("should simulate streaming behavior with partial log entries", () => {
// Simulate streaming chunks
const chunks = [
"<log_message>\n",
"<message>This is a debug level log message</message>\n",
"<level>debug</level>\n",
"</log_message>",
]

let accumulatedMessage = ""
const results = []

// Process each chunk as it would happen during streaming
for (const chunk of chunks) {
accumulatedMessage += chunk
const result = parseAssistantMessage(accumulatedMessage)
results.push(result)
}

// First chunk: Just the opening tag - filter out empty text blocks
const filteredResults0 = results[0].filter((block) => !(block.type === "text" && block.content === ""))
expect(filteredResults0[0]).toEqual({
type: "log_message",
message: "",
level: "info", // Default level
partial: true,
})

// Second chunk: Has message but not level - filter out empty text blocks
const filteredResults1 = results[1].filter((block) => !(block.type === "text" && block.content === ""))
expect(filteredResults1[0]).toEqual({
type: "log_message",
message: "This is a debug level log message</message>",
level: "info", // Still default level
partial: true,
})

// Third chunk: Has message and level but not closing tag - filter out empty text blocks
const filteredResults2 = results[2].filter((block) => !(block.type === "text" && block.content === ""))
expect(filteredResults2[0]).toEqual({
type: "log_message",
message: "This is a debug level log message</message>\n<level>debug</level>",
level: "info", // Still default level at this point
partial: true,
})

// Fourth chunk: Complete log entry - filter out empty text blocks
const filteredResults3 = results[3].filter((block) => !(block.type === "text" && block.content === ""))
expect(filteredResults3[0]).toEqual({
type: "log_message",
message: "This is a debug level log message",
level: "debug",
partial: false,
})
})

it("should handle multiple log entries with different levels", () => {
const message = `<log_message>
<message>This is a debug message</message>
<level>debug</level>
</log_message>

<log_message>
<message>This is an info message</message>
</log_message>

<log_message>
<message>This is a warning message</message>
<level>warn</level>
</log_message>

<log_message>
<message>This is an error message</message>
<level>error</level>
</log_message>`

const result = parseAssistantMessage(message)

// Filter out empty text blocks
const filteredResult = result.filter((block) => !(block.type === "text" && block.content === ""))

expect(filteredResult).toHaveLength(4)
expect(filteredResult[0]).toEqual({
type: "log_message",
message: "This is a debug message",
level: "debug",
partial: false,
})
expect(filteredResult[1]).toEqual({
type: "log_message",
message: "This is an info message",
level: "info", // Default level
partial: false,
})
expect(filteredResult[2]).toEqual({
type: "log_message",
message: "This is a warning message",
level: "warn",
partial: false,
})
expect(filteredResult[3]).toEqual({
type: "log_message",
message: "This is an error message",
level: "error",
partial: false,
})
})
})
12 changes: 12 additions & 0 deletions src/core/assistant-message/directives/log-directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { logLevels } from "../../../schemas"

/**
* Represents a log message directive from the assistant to the system.
* This directive instructs the system to record a message to its internal logs.
*/
export interface LogDirective {
type: "log_message"
message: string
level: (typeof logLevels)[number]
partial: boolean
}
1 change: 1 addition & 0 deletions src/core/assistant-message/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { type AssistantMessageContent, parseAssistantMessage } from "./parse-assistant-message"
export { type LogDirective } from "./directives/log-directive"
Loading