diff --git a/packages/agent/src/compaction/messages.ts b/packages/agent/src/compaction/messages.ts index 62d6c7879..93ae21b4f 100644 --- a/packages/agent/src/compaction/messages.ts +++ b/packages/agent/src/compaction/messages.ts @@ -156,7 +156,7 @@ export function defaultConvertToLlm(messages: AgentMessage[]): Message[] { ? [{ type: "text" as const, text: message.content }] : message.content; return { - role: "user", + role: "developer", content, attribution: message.attribution, timestamp: message.timestamp, diff --git a/packages/agent/src/compaction/openai.ts b/packages/agent/src/compaction/openai.ts index d74788015..9f37e2468 100644 --- a/packages/agent/src/compaction/openai.ts +++ b/packages/agent/src/compaction/openai.ts @@ -158,37 +158,11 @@ function shouldTrimOpenAiCompactInputItem(item: Record): boolea return item.type === "function_call_output" || (item.type === "message" && item.role === "developer"); } -function shouldKeepOpenAiCompactOutputUserMessage(item: Record): boolean { - if (item.role !== "user") return false; - const content = item.content; - if (!Array.isArray(content) || content.length === 0) return false; - const contextualFragmentPatterns = [ - [/^[\s\S]*<\/system-reminder>$/i, //i], - [/^#\s*AGENTS\.md instructions for\b[\s\S]*<\/INSTRUCTIONS>$/i, /# AGENTS.md instructions/], - [/^[\s\S]*<\/environment-context>$/i, //i], - [/^[\s\S]*<\/skill>$/i, //i], - [/^[\s\S]*<\/user-shell-command>$/i, //i], - [/^[\s\S]*<\/subagent-notification>$/i, //i], - ] as const; - return content.every(part => { - if (!part || typeof part !== "object") return false; - const candidate = part as { type?: unknown; text?: unknown }; - if (candidate.type === "input_image") return true; - if (candidate.type !== "input_text" || typeof candidate.text !== "string") return false; - const trimmed = candidate.text.trim(); - if (trimmed.length === 0) return false; - return !contextualFragmentPatterns.some(([strictPattern, markerPattern]) => { - return strictPattern.test(trimmed) || markerPattern.test(trimmed); - }); - }); -} function shouldKeepOpenAiCompactOutputItem(item: Record): boolean { if (item.type === "compaction" || item.type === "compaction_summary") return true; if (item.type !== "message") return false; - if (item.role === "developer") return false; - if (item.role === "assistant") return true; - return shouldKeepOpenAiCompactOutputUserMessage(item); + return item.role === "assistant" || item.role === "user"; } function trimOpenAiCompactInput( diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 922dcdbf2..ba041969a 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -2271,19 +2271,6 @@ function resolveAnthropicAdaptiveEffort( return mapEffortToAnthropicAdaptiveEffort(model, requestedEffort); } -function startsWithAfterAsciiWhitespace(value: string, prefix: string): boolean { - let index = 0; - while (index < value.length) { - const code = value.charCodeAt(index); - if (code !== 9 && code !== 10 && code !== 13 && code !== 32) break; - index++; - } - return value.startsWith(prefix, index); -} - -function isClaudeSyntheticUserText(value: string): boolean { - return startsWithAfterAsciiWhitespace(value, ""); -} function extractClaudeCodeFirstUserMessageText(messages: readonly Message[]): string { for (const message of messages) { @@ -2291,13 +2278,10 @@ function extractClaudeCodeFirstUserMessageText(messages: readonly Message[]): st const { content } = message; if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; - let fallback: string | undefined; for (const block of content) { - if (block.type !== "text") continue; - fallback ??= block.text; - if (!isClaudeSyntheticUserText(block.text)) return block.text; + if (block.type === "text") return block.text; } - return fallback ?? ""; + return ""; } return ""; } diff --git a/packages/coding-agent/src/session/messages.ts b/packages/coding-agent/src/session/messages.ts index 77c9af4ac..859b9f771 100644 --- a/packages/coding-agent/src/session/messages.ts +++ b/packages/coding-agent/src/session/messages.ts @@ -524,7 +524,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] { case "custom": case "hookMessage": { const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content; - const role = "user"; + const role = "developer"; const attribution = m.attribution; return { role, @@ -572,7 +572,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] { } } return { - role: "user", + role: "developer", content, attribution: "user", timestamp: m.timestamp, diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index 4b71c3da2..94d3f7a17 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -1501,6 +1501,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise { const remoteOutput = [ { type: "message", role: "developer", content: [{ type: "input_text", text: "stale developer" }] }, - { - type: "message", - role: "user", - content: [{ type: "input_text", text: "wrapped" }], - }, { type: "message", role: "user", content: [{ type: "input_text", text: "Real preserved user" }] }, { type: "reasoning", encrypted_content: "secret" }, { type: "function_call_output", call_id: "call_1", output: "ignored" }, diff --git a/packages/coding-agent/test/session-messages.test.ts b/packages/coding-agent/test/session-messages.test.ts index 220988027..4297b24db 100644 --- a/packages/coding-agent/test/session-messages.test.ts +++ b/packages/coding-agent/test/session-messages.test.ts @@ -14,7 +14,7 @@ function expectAttribution(message: Message | undefined, expected: "user" | "age } describe("convertToLlm custom message mapping", () => { - it("uses async-result attribution without special role mapping", () => { + it("maps custom messages to developer role with explicit agent attribution", () => { const messages: AgentMessage[] = [ { role: "custom", @@ -29,12 +29,12 @@ describe("convertToLlm custom message mapping", () => { const converted = convertToLlm(messages); expect(converted).toHaveLength(1); - expect(converted[0]?.role).toBe("user"); + expect(converted[0]?.role).toBe("developer"); expectAttribution(converted[0], "agent"); expect(inferCopilotInitiator(converted)).toBe("agent"); }); - it("preserves missing attribution for legacy custom messages", () => { + it("maps legacy custom messages to developer role", () => { const messages: AgentMessage[] = [ { role: "custom", @@ -48,17 +48,17 @@ describe("convertToLlm custom message mapping", () => { const converted = convertToLlm(messages); expect(converted).toHaveLength(1); - expect(converted[0]?.role).toBe("user"); + expect(converted[0]?.role).toBe("developer"); expectAttribution(converted[0], undefined); - expect(inferCopilotInitiator(converted)).toBe("user"); + expect(inferCopilotInitiator(converted)).toBe("agent"); }); it("uses explicit agent attribution for custom messages", () => { const messages: AgentMessage[] = [ { role: "custom", - customType: "ttsr-injection", - content: "Read file", + customType: "agent-reminder", + content: "Read file", display: false, attribution: "agent", timestamp: Date.now(), @@ -68,11 +68,33 @@ describe("convertToLlm custom message mapping", () => { const converted = convertToLlm(messages); expect(converted).toHaveLength(1); - expect(converted[0]?.role).toBe("user"); + expect(converted[0]?.role).toBe("developer"); expectAttribution(converted[0], "agent"); expect(inferCopilotInitiator(converted)).toBe("agent"); }); + it("maps file mention reminders to developer role", () => { + const messages: AgentMessage[] = [ + { + role: "fileMention", + files: [{ path: "src/config.ts", content: "export const config = {};" }], + timestamp: Date.now(), + }, + ]; + + const converted = convertToLlm(messages); + + expect(converted).toHaveLength(1); + expect(converted[0]?.role).toBe("developer"); + expectAttribution(converted[0], "user"); + if (converted[0]?.role !== "developer" || !Array.isArray(converted[0].content)) { + throw new Error("Expected developer array content"); + } + const text = converted[0].content.find(content => content.type === "text")?.text ?? ""; + expect(text).toContain(''); + expect(text).toContain("export const config = {};"); + }); + it("allows custom messages to opt into user attribution", () => { const messages: AgentMessage[] = [ { @@ -88,7 +110,7 @@ describe("convertToLlm custom message mapping", () => { const converted = convertToLlm(messages); expect(converted).toHaveLength(1); - expect(converted[0]?.role).toBe("user"); + expect(converted[0]?.role).toBe("developer"); expectAttribution(converted[0], "user"); expect(inferCopilotInitiator(converted)).toBe("user"); });