mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
fix(cli): print JSON from paseo logs --json
paseo logs ignored the global --json flag and always printed the text
transcript. Print { agentId, items } for one-shot logs, and one timeline
item per line in --follow mode, with the follow notices on stderr so
stdout stays parseable.
This commit is contained in:
committed by
paseo-bot[bot]
parent
71dc9f44c9
commit
4fdd9eab4b
@@ -0,0 +1,70 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Command } from "commander";
|
||||
import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types";
|
||||
|
||||
const agentId = "11111111-1111-4111-8111-111111111111";
|
||||
const daemonTarget = { kind: "endpoint" as const, host: "example.test:12345" };
|
||||
const timeline: AgentTimelineItem[] = [
|
||||
{ type: "user_message", text: "say hi" },
|
||||
{ type: "reasoning", text: "Greet the user." },
|
||||
{ type: "assistant_message", text: "Hi!", messageId: "msg_1" },
|
||||
];
|
||||
const timelineEntries = timeline.map((item) => ({ item, timestamp: "2026-10-19T00:00:00.000Z" }));
|
||||
|
||||
vi.mock("../../utils/client.js", () => ({
|
||||
connectToDaemon: vi.fn(async () => ({
|
||||
fetchAgent: vi.fn(async () => ({ agent: { id: agentId } })),
|
||||
fetchAgentTimeline: vi.fn(async () => ({ entries: timelineEntries })),
|
||||
close: vi.fn(async () => undefined),
|
||||
})),
|
||||
}));
|
||||
|
||||
import { runLogsCommand } from "./logs.js";
|
||||
|
||||
async function captureStdout(run: () => Promise<void>): Promise<string> {
|
||||
const lines: string[] = [];
|
||||
const log = vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
|
||||
lines.push(args.join(" "));
|
||||
});
|
||||
try {
|
||||
await run();
|
||||
} finally {
|
||||
log.mockRestore();
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
describe("paseo logs --json", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("prints the timeline as JSON instead of the text transcript", async () => {
|
||||
const stdout = await captureStdout(() =>
|
||||
runLogsCommand("1111", { daemonTarget, json: true }, {} as Command),
|
||||
);
|
||||
|
||||
expect(JSON.parse(stdout)).toEqual({ agentId, items: timeline });
|
||||
});
|
||||
|
||||
it("applies --tail and --filter to the JSON items", async () => {
|
||||
const stdout = await captureStdout(() =>
|
||||
runLogsCommand(
|
||||
"1111",
|
||||
{ daemonTarget, json: true, tail: "1", filter: "text" },
|
||||
{} as Command,
|
||||
),
|
||||
);
|
||||
|
||||
expect(JSON.parse(stdout)).toEqual({ agentId, items: [timeline[2]] });
|
||||
});
|
||||
|
||||
it("still prints the text transcript without --json", async () => {
|
||||
const stdout = await captureStdout(() =>
|
||||
runLogsCommand("1111", { daemonTarget }, {} as Command),
|
||||
);
|
||||
|
||||
expect(() => JSON.parse(stdout)).toThrow();
|
||||
expect(stdout).toContain("Hi!");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types";
|
||||
import { createFollowTranscriptWriter, formatAgentActivityTranscript } from "./logs.js";
|
||||
import {
|
||||
createFollowJsonWriter,
|
||||
createFollowTranscriptWriter,
|
||||
formatAgentActivityTranscript,
|
||||
} from "./logs.js";
|
||||
|
||||
function collect(items: readonly AgentTimelineItem[], turnId?: string): string {
|
||||
let output = "";
|
||||
@@ -149,3 +153,31 @@ describe("createFollowTranscriptWriter", () => {
|
||||
expect(output).toBe("# Title\n\nBody\n");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFollowJsonWriter", () => {
|
||||
it("writes one JSON item per line and honours the filter", () => {
|
||||
let output = "";
|
||||
const writer = createFollowJsonWriter("tools", (chunk) => {
|
||||
output += chunk;
|
||||
});
|
||||
const toolCall = {
|
||||
type: "tool_call",
|
||||
callId: "call_1",
|
||||
name: "shell",
|
||||
status: "completed",
|
||||
detail: { type: "unknown", input: null, output: null },
|
||||
error: null,
|
||||
} as AgentTimelineItem;
|
||||
|
||||
writer.push({ type: "assistant_message", text: "Running it.", messageId: "msg_1" });
|
||||
writer.push(toolCall);
|
||||
writer.end();
|
||||
|
||||
expect(
|
||||
output
|
||||
.trimEnd()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line)),
|
||||
).toEqual([toolCall]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,6 +70,31 @@ export function formatAgentActivityTranscript(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks the items `--tail` keeps, the same way the transcript does: the last n
|
||||
* timeline items, before any of them are dropped from the curated view.
|
||||
*/
|
||||
export function selectTailItems(
|
||||
timelineItems: AgentTimelineItem[],
|
||||
tailCount?: number,
|
||||
): AgentTimelineItem[] {
|
||||
if (tailCount === undefined) {
|
||||
return timelineItems;
|
||||
}
|
||||
if (tailCount === 0) {
|
||||
return [];
|
||||
}
|
||||
return timelineItems.slice(-tailCount);
|
||||
}
|
||||
|
||||
export function formatAgentLogsJson(
|
||||
agentId: string,
|
||||
timelineItems: AgentTimelineItem[],
|
||||
tailCount?: number,
|
||||
): string {
|
||||
return JSON.stringify({ agentId, items: selectTailItems(timelineItems, tailCount) }, null, 2);
|
||||
}
|
||||
|
||||
type StreamingTextType = "assistant_message" | "reasoning";
|
||||
|
||||
const THOUGHT_PREFIX = "[Thought] ";
|
||||
@@ -198,6 +223,21 @@ export function createFollowTranscriptWriter(
|
||||
};
|
||||
}
|
||||
|
||||
/** Follow mode with `--json`: one JSON timeline item per line (NDJSON). */
|
||||
export function createFollowJsonWriter(
|
||||
filter?: string,
|
||||
write: (chunk: string) => void = (chunk) => void process.stdout.write(chunk),
|
||||
): FollowTranscriptWriter {
|
||||
return {
|
||||
push(item) {
|
||||
if (matchesFilter(item, filter)) {
|
||||
write(`${JSON.stringify(item)}\n`);
|
||||
}
|
||||
},
|
||||
end() {},
|
||||
};
|
||||
}
|
||||
|
||||
function parseTailCount(raw: string | undefined): number | undefined {
|
||||
if (raw === undefined) return undefined;
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
@@ -306,6 +346,11 @@ export async function runLogsCommand(
|
||||
|
||||
await client.close();
|
||||
|
||||
if (options.json) {
|
||||
console.log(formatAgentLogsJson(resolvedId, timelineItems, tailCount));
|
||||
return;
|
||||
}
|
||||
|
||||
// Use curateAgentActivity to format the transcript
|
||||
if (tailCount === 0) {
|
||||
return;
|
||||
@@ -350,8 +395,14 @@ async function runFollowMode({
|
||||
existingItems = existingItems.filter((item) => matchesFilter(item, options.filter));
|
||||
}
|
||||
|
||||
const json = options.json === true;
|
||||
|
||||
// Print existing transcript (tail-like behavior)
|
||||
if (tailCount > 0) {
|
||||
if (json) {
|
||||
for (const item of selectTailItems(existingItems, tailCount)) {
|
||||
console.log(JSON.stringify(item));
|
||||
}
|
||||
} else if (tailCount > 0) {
|
||||
const existingTranscript = formatAgentActivityTranscript(existingItems, tailCount);
|
||||
if (existingTranscript !== NO_ACTIVITY_MESSAGE) {
|
||||
console.log(existingTranscript);
|
||||
@@ -362,12 +413,16 @@ async function runFollowMode({
|
||||
const tailLabel =
|
||||
tailCount === 0 ? "no history" : `last ${tailCount} entr${tailCount === 1 ? "y" : "ies"}`;
|
||||
|
||||
const writer = createFollowTranscriptWriter(undefined, options.filter);
|
||||
const writer = json
|
||||
? createFollowJsonWriter(options.filter)
|
||||
: createFollowTranscriptWriter(undefined, options.filter);
|
||||
// Keep stdout parseable in JSON mode: one timeline item per line, notices on stderr.
|
||||
const notice = json ? console.error : console.log;
|
||||
|
||||
const unsubscribe = client.subscribeAgentTimeline(agentId, (message) => {
|
||||
if (message.type === "agent.timeline.replacement") {
|
||||
writer.end();
|
||||
console.log("\n[Timeline replaced; earlier output is no longer current]");
|
||||
notice("\n[Timeline replaced; earlier output is no longer current]");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -378,7 +433,7 @@ async function runFollowMode({
|
||||
}
|
||||
if (message.type === "agent.timeline.subscription_restored") {
|
||||
writer.end();
|
||||
console.log("\n[Reconnected; live output resumed. Events may have been missed.]");
|
||||
notice("\n[Reconnected; live output resumed. Events may have been missed.]");
|
||||
return;
|
||||
}
|
||||
if (message.payload.event.type === "timeline") {
|
||||
@@ -399,7 +454,7 @@ async function runFollowMode({
|
||||
});
|
||||
|
||||
await unsubscribe.ready;
|
||||
console.log(`\n--- Following logs (${tailLabel}; Ctrl+C to stop) ---\n`);
|
||||
notice(`\n--- Following logs (${tailLabel}; Ctrl+C to stop) ---\n`);
|
||||
|
||||
await waitForStop();
|
||||
writer.end();
|
||||
|
||||
Reference in New Issue
Block a user