perf(agents): build converted messages in one array (#142450)

This commit is contained in:
Peter Steinberger 2026-09-08 13:11:12 -07:00 • committed by GitHub
parent 10e539b174
commit 27ede01d26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 160 additions and 95 deletions

View file

@ -1417,7 +1417,6 @@ packages/acp-core/src/types.ts 2
packages/agent-core/src/agent-loop.ts 3
packages/agent-core/src/harness/compaction/branch-summarization.ts 1
packages/agent-core/src/harness/compaction/compaction.ts 2
packages/agent-core/src/harness/messages.ts 2
packages/agent-core/src/harness/session/tool-result-pairing.ts 17
packages/agent-core/src/harness/session/uuid.ts 1
packages/agent-core/src/harness/utils/truncate.ts 1

View file

@ -1,7 +1,103 @@
// Agent Core tests cover messages behavior.
import type { Message } from "@openclaw/llm-core";
import { describe, expect, it } from "vitest";
import type { AgentMessage } from "../types.js";
import { convertToLlm, createCustomMessage } from "./messages.js";
describe("convertToLlm message ownership", () => {
it("preserves standard message objects and their private metadata", () => {
const user: Message = { role: "user", content: "question", timestamp: 1 };
const identity = Symbol("message identity");
Object.defineProperty(user, identity, { value: "original", enumerable: false });
const messages: Message[] = [
user,
{
role: "assistant",
content: [{ type: "text", text: "answer" }],
api: "openai-responses",
provider: "openai",
model: "gpt-5.6-sol",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: 2,
},
{
role: "toolResult",
toolCallId: "call",
toolName: "fixture",
content: [{ type: "text", text: "result" }],
isError: false,
timestamp: 3,
},
];
const converted = convertToLlm(messages);
expect(converted).not.toBe(messages);
expect(converted).toHaveLength(messages.length);
messages.forEach((message, index) => expect(converted[index]).toBe(message));
expect(Object.getOwnPropertyDescriptor(converted[0], identity)).toEqual(
Object.getOwnPropertyDescriptor(user, identity),
);
});
it.each([false, true])("preserves custom content ownership with carrier=%s", (carrier) => {
const timestamp = "2026-05-30T17:00:00.000Z";
const blocks = [{ type: "text" as const, text: "array content" }];
const customType = carrier ? "openclaw.runtime-context" : "note";
const details = carrier
? { source: "openclaw-runtime-context", runtimeContextCarrier: true }
: { source: "other" };
const arrayMessage = createCustomMessage(customType, blocks, false, details, timestamp);
const textMessage = createCustomMessage(customType, "text content", false, details, timestamp);
const [array, text] = convertToLlm([arrayMessage, textMessage]);
const [repeatedText] = convertToLlm([textMessage]);
expect(array).not.toBe(arrayMessage);
expect(array?.content).toBe(blocks);
expect(array).toEqual({
role: "user",
content: blocks,
timestamp: Date.parse(timestamp),
...(carrier ? { runtimeContextCarrier: true } : {}),
});
expect(text).not.toBe(textMessage);
expect(text).toEqual({
role: "user",
content: [{ type: "text", text: "text content" }],
timestamp: Date.parse(timestamp),
...(carrier ? { runtimeContextCarrier: true } : {}),
});
expect(text?.content).not.toBe(repeatedText?.content);
});
it("skips array holes and does not visit messages appended during conversion", () => {
const messages: AgentMessage[] = [];
messages.length = 3;
const appended: AgentMessage = { role: "user", content: "later", timestamp: 2 };
const first: AgentMessage = {
get role() {
messages.push(appended);
return "user" as const;
},
content: "first",
timestamp: 1,
};
messages[1] = first;
const converted = convertToLlm(messages);
expect(converted).toHaveLength(1);
expect(converted[0]).toBe(first);
expect(messages).toHaveLength(4);
});
});
describe("harness message timestamps", () => {
it("rejects invalid timestamps before creating context messages", () => {
expect(() => createCustomMessage("note", "content", true, {}, "not-a-date")).toThrow(
@ -55,31 +151,3 @@ describe("harness message timestamps", () => {
expect(message?.timestamp).toBe(0);
});
});
describe("convertToLlm runtime-context carrier marking", () => {
const timestamp = "2026-05-30T17:00:00.000Z";
it("marks a runtime-context carrier custom message so providers skip cache anchoring", () => {
const [message] = convertToLlm([
createCustomMessage(
"openclaw.runtime-context",
"current-turn metadata",
false,
{ source: "openclaw-runtime-context", runtimeContextCarrier: true },
timestamp,
),
]);
expect(message?.role).toBe("user");
expect((message as { runtimeContextCarrier?: boolean }).runtimeContextCarrier).toBe(true);
});
it("does not mark ordinary custom messages", () => {
const [message] = convertToLlm([
createCustomMessage("note", "some note", false, { source: "other" }, timestamp),
]);
expect(message?.role).toBe("user");
expect((message as { runtimeContextCarrier?: boolean }).runtimeContextCarrier).toBeUndefined();
});
});

View file

@ -17,17 +17,12 @@ export type {
} from "../types.js";
/** Harness-only transcript entries that can be normalized into LLM messages. */
export type HarnessMessage =
| AgentMessage
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage;
export type HarnessMessage = AgentMessage;
// Internal session paths keep call sites explicit about this harness-owned
// boundary even though these message roles are part of AgentMessage.
export function asAgentMessage(message: HarnessMessage): AgentMessage {
return message as AgentMessage;
return message;
}
function requireSessionTimestampMs(value: string, label: string): number {
@ -136,65 +131,68 @@ export function isRuntimeContextCarrier(message: AgentMessage): boolean {
/** Convert harness transcript messages into the LLM-facing message sequence. */
export function convertToLlm(messages: AgentMessage[]): Message[] {
return messages
.map((m): Message | undefined => {
const message = m as HarnessMessage;
switch (message.role) {
case "bashExecution":
if (message.excludeFromContext) {
return undefined;
}
return {
role: "user",
content: [{ type: "text", text: bashExecutionToText(message) }],
timestamp: message.timestamp,
};
case "custom": {
if (message.excludeFromContext) {
return undefined;
}
const content =
typeof message.content === "string"
? [{ type: "text" as const, text: message.content }]
: message.content;
// Preserve carrier identity so provider-owned replay and cache policy
// can distinguish transient context from append-only context.
return {
role: "user",
content,
timestamp: message.timestamp,
...(isRuntimeContextCarrier(message) ? { runtimeContextCarrier: true } : {}),
};
const llmMessages: Message[] = [];
// Preserve map's hole skipping and captured length without its intermediate array.
messages.forEach((message) => {
switch (message.role) {
case "bashExecution":
if (message.excludeFromContext) {
return;
}
case "branchSummary":
return {
role: "user",
content: [
{
type: "text" as const,
text: BRANCH_SUMMARY_PREFIX + message.summary + BRANCH_SUMMARY_SUFFIX,
},
],
timestamp: message.timestamp,
};
case "compactionSummary":
return {
role: "user",
content: [
{
type: "text" as const,
text: COMPACTION_SUMMARY_PREFIX + message.summary + COMPACTION_SUMMARY_SUFFIX,
},
],
timestamp: normalizeCompactionSummaryTimestamp(message.timestamp),
};
case "user":
case "assistant":
case "toolResult":
return message;
default:
return undefined;
llmMessages.push({
role: "user",
content: [{ type: "text", text: bashExecutionToText(message) }],
timestamp: message.timestamp,
});
break;
case "custom": {
if (message.excludeFromContext) {
return;
}
const content =
typeof message.content === "string"
? [{ type: "text" as const, text: message.content }]
: message.content;
// Preserve carrier identity so provider-owned replay and cache policy
// can distinguish transient context from append-only context.
llmMessages.push({
role: "user",
content,
timestamp: message.timestamp,
...(isRuntimeContextCarrier(message) ? { runtimeContextCarrier: true } : {}),
});
break;
}
})
.filter((m): m is Message => m !== undefined);
case "branchSummary":
llmMessages.push({
role: "user",
content: [
{
type: "text" as const,
text: BRANCH_SUMMARY_PREFIX + message.summary + BRANCH_SUMMARY_SUFFIX,
},
],
timestamp: message.timestamp,
});
break;
case "compactionSummary":
llmMessages.push({
role: "user",
content: [
{
type: "text" as const,
text: COMPACTION_SUMMARY_PREFIX + message.summary + COMPACTION_SUMMARY_SUFFIX,
},
],
timestamp: normalizeCompactionSummaryTimestamp(message.timestamp),
});
break;
case "user":
case "assistant":
case "toolResult":
llmMessages.push(message);
break;
}
});
return llmMessages;
}