Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions plugins/tracing/src/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ import type {
} from "./types.js";
import { debugLog, toText } from "./utils.js";

const MAX_REPEATED_INPUT_BYTES = 64 * 1024 * 1024;

async function loadSession(file: string): Promise<RolloutLine[]> {
const lines: RolloutLine[] = [];
const input = createReadStream(file, { encoding: "utf-8" });
for await (const raw of createInterface({ input, crlfDelay: Infinity })) {
const trimmed = raw.trim();
if (!trimmed) continue;
try {
lines.push(JSON.parse(trimmed) as RolloutLine);
const line = JSON.parse(trimmed) as RolloutLine;
if (line.type !== "compacted") lines.push(line);
} catch {
// skip malformed lines rather than aborting the whole upload
}
Expand Down Expand Up @@ -435,6 +438,7 @@ async function emitTurn(
unannouncedSubagents?: SubagentRollout[];
inheritableTurnIds?: ReadonlySet<string>;
historyPrefix?: ChatMlMessage[];
omitRepeatedInput: boolean;
},
): Promise<void> {
const isSubagent = sessionMeta.isSubagentThread === true || ctx.parentObservation != null;
Expand Down Expand Up @@ -492,10 +496,18 @@ async function emitTurn(
const generation = startObservation(
isSubagent ? "LLM Subagent" : "LLM",
{
input: attachToolDefinitions(
generationInput(systemMessage, historyPrefix, turn, i),
turn.toolDefinitions,
),
input: ctx.omitRepeatedInput
? [
{
role: "system",
content:
"[Generation input omitted from trace: estimated repeated context exceeds 64 MB]",
},
]
: attachToolDefinitions(
generationInput(systemMessage, historyPrefix, turn, i),
turn.toolDefinitions,
),
output: buildGenerationOutput(step),
model: turn.model,
...(turn.reasoningEffort
Expand Down Expand Up @@ -634,10 +646,15 @@ export async function convertRollout(
stoppedTurnId?: string;
},
): Promise<string[]> {
const rolloutBytes = (await fs.stat(rolloutFile)).size;
const { sessionMeta, turns } = parseSession(await loadSession(rolloutFile));
// ponytail: file size overestimates prompt size; count parsed message bytes if this hides prompts too often.
const omitRepeatedInput =
rolloutBytes * turns.reduce((total, turn) => total + turn.steps.length, 0) >
MAX_REPEATED_INPUT_BYTES;

const historyPrefixes: ChatMlMessage[][] = [];
{
if (!omitRepeatedInput) {
const seen: ChatMlMessage[] = [];
for (const turn of turns) {
historyPrefixes.push([...seen]);
Expand Down Expand Up @@ -710,6 +727,7 @@ export async function convertRollout(
unannouncedSubagents: subagentsFor(turnIndex),
inheritableTurnIds,
historyPrefix: historyPrefixes[turnIndex],
omitRepeatedInput,
});
}
return [];
Expand Down Expand Up @@ -747,6 +765,7 @@ export async function convertRollout(
unannouncedSubagents: subagentsFor(turnIndex),
inheritableTurnIds,
historyPrefix: historyPrefixes[turnIndex],
omitRepeatedInput,
});

if (attached) {
Expand Down
56 changes: 56 additions & 0 deletions plugins/tracing/test/trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,62 @@ describe("convertRollout", () => {
fs.rmSync(dir, { recursive: true, force: true });
});

it("bounds repeated generation input while keeping a large one-step prompt", async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lf-codex-long-"));
const day = path.join(dir, "sessions", "2026", "06", "03");
fs.mkdirSync(day, { recursive: true });
const timestamp = "2026-06-03T09:00:00Z";
for (const steps of [1, 80]) {
const file = path.join(day, `rollout-${steps}.jsonl`);
const lines: unknown[] = [
{ timestamp, type: "session_meta", payload: { id: `sess-${steps}` } },
{ timestamp, type: "event_msg", payload: { type: "task_started", turn_id: "t1" } },
{
timestamp,
type: "event_msg",
payload: { type: "user_message", message: "x".repeat(1_000_000) },
},
];
for (let step = 0; step < steps; step++)
lines.push(
{
timestamp,
type: "response_item",
payload: {
type: "message",
role: "assistant",
content: [{ type: "output_text", text: `Answer ${step}` }],
},
},
{ timestamp, type: "event_msg", payload: { type: "token_count", info: {} } },
);
lines.push({
timestamp,
type: "event_msg",
payload: { type: "task_complete", turn_id: "t1" },
});
fs.writeFileSync(file, lines.map((line) => JSON.stringify(line)).join("\n"));
await convertAndMark(file, { config: baseConfig, stoppedTurnId: "t1" });
const generations = exporter
.getFinishedSpans()
.filter((span) => obsType(span) === "generation");
expect(generations).toHaveLength(steps);
const input = JSON.parse(attr(generations.at(-1)!, "langfuse.observation.input"));
if (steps === 1) expect(input[0].content).toHaveLength(1_000_000);
else
expect(input).toEqual([
{
role: "system",
content:
"[Generation input omitted from trace: estimated repeated context exceeds 64 MB]",
},
]);
expect(attr(turnRoots()[0]!, "langfuse.observation.input")).toHaveLength(1_000_000);
exporter.reset();
}
fs.rmSync(dir, { recursive: true, force: true });
});

it("sends attached images as multimodal content and the loaded tools on the call", async () => {
const URI = "data:image/png;base64,iVBORw0KGgo=";
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lf-codex-media-"));
Expand Down