fix(agent): preserve reasoning order in activity transcripts (#6546)

Format projected text entries in sequence instead of grouping replies ahead of their preceding reasoning. Timeline projection already owns fragment merging.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-11 05:00:02 +02:00
committed by GitHub
parent 446749102e
commit 7e66657bf0
3 changed files with 59 additions and 34 deletions
@@ -15,6 +15,18 @@ function collect(items: readonly AgentTimelineItem[], turnId?: string): string {
}
describe("createFollowTranscriptWriter", () => {
it("matches one-shot logs when reasoning and assistant text alternate", () => {
const items: AgentTimelineItem[] = [
{ type: "reasoning", text: "Check " },
{ type: "reasoning", text: "the commands." },
{ type: "assistant_message", text: "I will list the commands.", messageId: "msg_1" },
{ type: "reasoning", text: "Then summarize." },
{ type: "assistant_message", text: "Done.", messageId: "msg_2" },
];
expect(collect(items).trimEnd()).toBe(formatAgentActivityTranscript(items));
});
it("keeps a word that arrives split across fragments intact", () => {
const output = collect([
{ type: "assistant_message", text: "the paseo-pr", messageId: "msg_1" },
@@ -39,6 +39,45 @@ function row(seq: number, item: AgentTimelineItem): AgentTimelineRow {
}
describe("curateAgentActivity", () => {
it("keeps reasoning before the assistant text that followed it", () => {
const timeline: AgentTimelineItem[] = [
{ type: "reasoning", text: "Check the commands first." },
{ type: "assistant_message", text: "I will list the commands." },
toolCallItem({
callId: "shell-order",
name: "Bash",
detail: {
type: "shell",
command: "ls packages/cli/src/commands/agent/",
output: "",
exitCode: 0,
},
}),
];
expect(curateAgentActivity(timeline)).toBe(
"[Thought] Check the commands first.\nI will list the commands.\n[Shell] ls packages/cli/src/commands/agent/",
);
});
it("preserves alternating text kinds, labels, and fragment concatenation", () => {
const timeline: AgentTimelineItem[] = [
{ type: "assistant_message", text: "First message." },
{ type: "reasoning", text: "First " },
{ type: "reasoning", text: "thought." },
{ type: "assistant_message", text: "Second " },
{ type: "assistant_message", text: "message." },
{ type: "reasoning", text: "Second thought." },
];
expect(curateAgentActivity(timeline, { labelAssistantMessages: true })).toBe(
"[Assistant] First message.\n[Thought] First thought.\n[Assistant] Second message.\n[Thought] Second thought.",
);
expect(curateAgentActivity(timeline, { maxItems: 2 })).toBe(
"Second message.\n[Thought] Second thought.",
);
});
it("renders user/assistant/reasoning entries", () => {
const timeline: AgentTimelineItem[] = [
{ type: "user_message", text: "Hello" },
@@ -25,36 +25,20 @@ interface ActivityEntry {
type TextAgentAttachment = Extract<AgentAttachment, { type: "text" }>;
function appendText(buffer: string, text: string): string {
const normalized = text.trim();
if (!normalized) {
return buffer;
}
if (!buffer) {
return normalized;
}
return `${buffer}\n${normalized}`;
}
function activityEntry(text: string): ActivityEntry {
return { text };
}
function flushBuffers(
function appendTextEntry(
entries: ActivityEntry[],
buffers: { message: string; thought: string },
item: Extract<AgentTimelineItem, { type: "assistant_message" | "reasoning" }>,
options?: ActivityCuratorOptions,
) {
if (buffers.message.trim()) {
const text = buffers.message.trim();
entries.push(activityEntry(options?.labelAssistantMessages ? `[Assistant] ${text}` : text));
}
if (buffers.thought.trim()) {
const text = buffers.thought.trim();
entries.push(activityEntry(`[Thought] ${text}`));
}
buffers.message = "";
buffers.thought = "";
const text = item.text.trim();
if (!text) return;
let prefix = options?.labelAssistantMessages ? "[Assistant] " : "";
if (item.type === "reasoning") prefix = "[Thought] ";
entries.push(activityEntry(`${prefix}${text}`));
}
function formatToolInputJson(input: unknown): string | null {
@@ -147,7 +131,6 @@ function curateProjectedActivityEntries(
const recentItems = maxItems > 0 && items.length > maxItems ? items.slice(-maxItems) : items;
const entries: ActivityEntry[] = [];
const buffers = { message: "", thought: "" };
for (const item of recentItems) {
if (!shouldIncludeItem(item, options)) {
@@ -156,17 +139,13 @@ function curateProjectedActivityEntries(
switch (item.type) {
case "user_message":
flushBuffers(entries, buffers, options);
entries.push(activityEntry(`[User] ${item.text.trim()}`));
break;
case "assistant_message":
buffers.message = appendText(buffers.message, item.text);
break;
case "reasoning":
buffers.thought = appendText(buffers.thought, item.text);
appendTextEntry(entries, item, options);
break;
case "tool_call": {
flushBuffers(entries, buffers, options);
entries.push(formatToolCallEntry(item, options));
if (item.detail.type === "sub_agent" && item.detail.log.trim()) {
entries.push(activityEntry(item.detail.log.trim()));
@@ -174,7 +153,6 @@ function curateProjectedActivityEntries(
break;
}
case "todo":
flushBuffers(entries, buffers, options);
entries.push(activityEntry("[Tasks]"));
for (const entry of item.items) {
const checkbox = entry.completed ? "[x]" : "[ ]";
@@ -183,18 +161,14 @@ function curateProjectedActivityEntries(
}
break;
case "error":
flushBuffers(entries, buffers, options);
entries.push(activityEntry(`[Error] ${item.message}`));
break;
case "compaction":
flushBuffers(entries, buffers, options);
entries.push(activityEntry("[Compacted]"));
break;
}
}
flushBuffers(entries, buffers, options);
return entries;
}