mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user