diff --git a/plugins/tracing/src/trace.ts b/plugins/tracing/src/trace.ts index ab5c43d..1ec7510 100644 --- a/plugins/tracing/src/trace.ts +++ b/plugins/tracing/src/trace.ts @@ -31,6 +31,8 @@ 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 { const lines: RolloutLine[] = []; const input = createReadStream(file, { encoding: "utf-8" }); @@ -38,7 +40,8 @@ async function loadSession(file: string): Promise { 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 } @@ -435,6 +438,7 @@ async function emitTurn( unannouncedSubagents?: SubagentRollout[]; inheritableTurnIds?: ReadonlySet; historyPrefix?: ChatMlMessage[]; + omitRepeatedInput: boolean; }, ): Promise { const isSubagent = sessionMeta.isSubagentThread === true || ctx.parentObservation != null; @@ -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 @@ -634,10 +646,15 @@ export async function convertRollout( stoppedTurnId?: string; }, ): Promise { + 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]); @@ -710,6 +727,7 @@ export async function convertRollout( unannouncedSubagents: subagentsFor(turnIndex), inheritableTurnIds, historyPrefix: historyPrefixes[turnIndex], + omitRepeatedInput, }); } return []; @@ -747,6 +765,7 @@ export async function convertRollout( unannouncedSubagents: subagentsFor(turnIndex), inheritableTurnIds, historyPrefix: historyPrefixes[turnIndex], + omitRepeatedInput, }); if (attached) { diff --git a/plugins/tracing/test/trace.test.ts b/plugins/tracing/test/trace.test.ts index aa28b51..e3381bf 100644 --- a/plugins/tracing/test/trace.test.ts +++ b/plugins/tracing/test/trace.test.ts @@ -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-"));