mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
perf(agents): build converted messages in one array (#142450)
This commit is contained in:
parent
10e539b174
commit
27ede01d26
3 changed files with 160 additions and 95 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue