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:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user