fix(coding-agent): mapped auxiliary messages to developer role for compaction

- Updated `convertToLlm` logic to emit `developer` role for custom, hook, and file-mention inputs.
- Simplified OpenAI compact output filtering to retain only `user` and `assistant` messages, removing legacy `system-reminder` pattern checks.
- Adjusted compaction and session tests to match the new developer-role mapping and expected compacted content.
This commit is contained in:
can1357
2026-06-08 05:18:45 +02:00
parent dfeb9af61a
commit e13f2de58a
7 changed files with 38 additions and 62 deletions
+1 -1
View File
@@ -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,
+1 -27
View File
@@ -158,37 +158,11 @@ function shouldTrimOpenAiCompactInputItem(item: Record<string, unknown>): boolea
return item.type === "function_call_output" || (item.type === "message" && item.role === "developer");
}
function shouldKeepOpenAiCompactOutputUserMessage(item: Record<string, unknown>): boolean {
if (item.role !== "user") return false;
const content = item.content;
if (!Array.isArray(content) || content.length === 0) return false;
const contextualFragmentPatterns = [
[/^<system-reminder>[\s\S]*<\/system-reminder>$/i, /<system-reminder>/i],
[/^#\s*AGENTS\.md instructions for\b[\s\S]*<\/INSTRUCTIONS>$/i, /# AGENTS.md instructions/],
[/^<environment-context>[\s\S]*<\/environment-context>$/i, /<environment-context>/i],
[/^<skill>[\s\S]*<\/skill>$/i, /<skill>/i],
[/^<user-shell-command>[\s\S]*<\/user-shell-command>$/i, /<user-shell-command>/i],
[/^<subagent-notification>[\s\S]*<\/subagent-notification>$/i, /<subagent-notification>/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<string, unknown>): 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(
+2 -18
View File
@@ -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, "<system-reminder>");
}
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 "";
}
@@ -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,
@@ -1501,6 +1501,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
await awaitAbortable(
session.prompt(reminder, {
attribution: "agent",
synthetic: true,
...(isFinalRetry && reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
}),
);
@@ -632,11 +632,6 @@ describe("remote compaction setting", () => {
const remoteOutput = [
{ type: "message", role: "developer", content: [{ type: "input_text", text: "stale developer" }] },
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "<system-reminder>wrapped</system-reminder>" }],
},
{ 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" },
@@ -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: "<system-reminder>Read file</system-reminder>",
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('<file path="src/config.ts">');
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");
});