fix: preserve inherited fork context and live history attachments

This commit is contained in:
wengshihao
2026-11-06 13:24:38 +08:00
parent 5d71206b72
commit 5800a626b9
10 changed files with 260 additions and 64 deletions
@@ -572,3 +572,57 @@ it("preserves every message in fork history below the character budget", () => {
`<chat-history-summary>\nChat history from a previous Paseo agent.\n\n[User] ${text}\n</chat-history-summary>`,
);
});
it.each(["", "Continue in B"])("carries inherited history through repeated forks (%s)", (text) => {
const original = buildAgentForkContextAttachment({
rows: [
row(1, { type: "user_message", text: "Question in A" }),
row(2, { type: "assistant_message", text: "Answer in A" }),
],
});
const child = buildAgentForkContextAttachment({
rows: [
row(1, {
type: "user_message",
text,
chatHistory: { ...original.attachment, contextKind: "chat_history" },
}),
row(2, { type: "assistant_message", text: "Answer in B" }),
],
});
const grandchild = buildAgentForkContextAttachment({
rows: [
row(1, {
type: "user_message",
text: "Continue in C",
chatHistory: { ...child.attachment, contextKind: "chat_history" },
}),
row(2, { type: "assistant_message", text: "Answer in C" }),
],
});
expect(grandchild.attachment.text).toContain("[User] Question in A\n[Assistant] Answer in A");
expect(grandchild.attachment.text).toContain("[Assistant] Answer in B");
expect(grandchild.attachment.text).toContain("[User] Continue in C\n[Assistant] Answer in C");
expect(grandchild.attachment.text.match(/<chat-history-summary>/g)).toHaveLength(1);
});
it("keeps the fork budget when inherited history exceeds the remaining space", () => {
const result = buildAgentForkContextAttachment({
rows: [
row(1, {
type: "user_message",
text: "Current question",
chatHistory: {
type: "text",
mimeType: "text/plain",
contextKind: "chat_history",
text: "Old ".repeat(30_000),
},
}),
row(2, { type: "assistant_message", text: "Latest answer" }),
],
});
expect(result.attachment.text.length).toBeLessThanOrEqual(100_000);
expect(result.attachment.text).toContain("[Earlier chat history omitted]");
expect(result.attachment.text).toContain("[User] Current question\n[Assistant] Latest answer");
});
@@ -1,3 +1,4 @@
import { extractChatHistoryBody } from "@getpaseo/protocol/chat-history";
import type { AgentTimelineItem } from "./agent-sdk-types.js";
import type { AgentMessage } from "@getpaseo/protocol/agent-message";
import type { AgentAttachment } from "@getpaseo/protocol/messages";
@@ -21,6 +22,7 @@ const FORK_CONTEXT_KINDS: readonly AgentTimelineItem["type"][] = [
];
interface ActivityCuratorOptions {
includeChatHistory?: boolean;
maxItems?: number;
labelAssistantMessages?: boolean;
includeKinds?: readonly AgentTimelineItem["type"][];
@@ -37,6 +39,17 @@ function activityEntry(text: string): ActivityEntry {
return { text };
}
function appendUserEntry(
entries: ActivityEntry[],
item: Extract<AgentTimelineItem, { type: "user_message" }>,
options?: ActivityCuratorOptions,
) {
if (options?.includeChatHistory && item.chatHistory) {
entries.push(activityEntry(extractChatHistoryBody(item.chatHistory.text)));
}
entries.push(activityEntry(`[User] ${item.text.trim()}`));
}
function appendTextEntry(
entries: ActivityEntry[],
item: Extract<AgentTimelineItem, { type: "assistant_message" | "reasoning" }>,
@@ -158,7 +171,7 @@ function curateProjectedActivityEntries(
switch (item.type) {
case "user_message":
entries.push(activityEntry(`[User] ${item.text.trim()}`));
appendUserEntry(entries, item, options);
break;
case "assistant_message":
case "reasoning":
@@ -361,6 +374,7 @@ export function buildAgentForkContextAttachment(input: {
});
const entries = curateProjectedActivityEntries(selected.items, {
maxItems: 0,
includeChatHistory: true,
labelAssistantMessages: true,
includeKinds: FORK_CONTEXT_KINDS,
includeExternalToolInput: false,