From f602d61eb29274138a529019ddedf3764032327f Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Thu, 8 Oct 2026 15:09:28 +0100 Subject: [PATCH 1/2] fix(framework): keep LLM spans non-empty during backgrounded commands --- .../scripts/upload-braintrust.test.ts | 48 +++++++++++++++++++ apps/framework/scripts/upload-braintrust.ts | 20 +++++--- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/apps/framework/scripts/upload-braintrust.test.ts b/apps/framework/scripts/upload-braintrust.test.ts index dcee13b3..de02b8ad 100644 --- a/apps/framework/scripts/upload-braintrust.test.ts +++ b/apps/framework/scripts/upload-braintrust.test.ts @@ -278,6 +278,54 @@ describe('logTranscript', () => { }); }); + it('starts LLM spans during a backgrounded command at the previous edge', () => { + const root: RecordedSpan = { children: [] }; + const message = (requestId: string, s: number) => ({ + type: 'message' as const, + role: 'assistant' as const, + content: requestId, + ts: (100 + s) * 1000, + requestId, + }); + logTranscript(recorder(root), { + prompt: 'go', + agentReport: '', + checks: [], + judgeCalls: [], + passed: true, + modelId: 'm', + startTime: 100, + toolLabels: [], + transcript: [ + { + type: 'tool_call', + name: 'Bash', + input: {}, + id: 't1', + ts: 104_000, + resultTs: 120_000, + requestId: 'r1', + }, + message('r2', 8), + message('r3', 12), + message('r4', 25), + ], + }); + expect( + root.children[0]?.children.map(({ name, start, end }) => [ + name, + start, + end, + ]) + ).toEqual([ + ['m', 100, 104], + ['Bash', 104, 120], + ['m', 104, 108], + ['m', 108, 112], + ['m', 120, 125], + ]); + }); + it('ends setup at a leading non-assistant message and starts task there', () => { const root: RecordedSpan = { children: [] }; logTranscript(recorder(root), { diff --git a/apps/framework/scripts/upload-braintrust.ts b/apps/framework/scripts/upload-braintrust.ts index be1e2d17..060c04ce 100644 --- a/apps/framework/scripts/upload-braintrust.ts +++ b/apps/framework/scripts/upload-braintrust.ts @@ -498,6 +498,15 @@ export function logTranscript( }); task.log({ input: row.prompt, output: row.agentReport }); let latestTime = taskStart; + // A backgrounded command can end after later requests, so each request + // starts at the latest edge before it. + const edges = taskStart === undefined ? [] : [taskStart]; + const mark = (time: number | undefined) => { + if (time !== undefined) { + edges.push(time); + } + latestTime = latest(latestTime, time); + }; let toolIndex = 0; // Seeded with the prompt, which the transcript omits. const history: Record[] = row.prompt @@ -522,10 +531,8 @@ export function logTranscript( open.usage ??= part.usage; return open; } - const startTime = - latestTime === undefined || at === undefined - ? (latestTime ?? at) - : Math.min(latestTime, at); + const before = at === undefined ? [] : edges.filter((edge) => edge <= at); + const startTime = before.length ? Math.max(...before) : (at ?? latestTime); const request: Request = { id, span: task.startSpan({ @@ -565,7 +572,7 @@ export function logTranscript( ? `${message.content}\n${part.content}` : part.content; } - latestTime = latest(latestTime, at); + mark(at); continue; } if (part.requestId) { @@ -586,7 +593,8 @@ export function logTranscript( } const label = row.toolLabels[toolIndex++]; const endTime = sec(part.resultTs) ?? at; - latestTime = latest(latestTime, endTime); + mark(at); + mark(endTime); const span = task.startSpan({ name: label ? `${part.name}: ${label}` : part.name, type: 'tool', From d188c754f841b66d6a177b5d6c4c361875b859ac Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues Date: Thu, 8 Oct 2026 15:19:01 +0100 Subject: [PATCH 2/2] docs(framework): describe LLM span start rule --- apps/framework/scripts/upload-braintrust.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/framework/scripts/upload-braintrust.ts b/apps/framework/scripts/upload-braintrust.ts index 060c04ce..e4386303 100644 --- a/apps/framework/scripts/upload-braintrust.ts +++ b/apps/framework/scripts/upload-braintrust.ts @@ -424,7 +424,7 @@ export interface SpanSink { * https://github.com/braintrustdata/braintrust-coding-agent-plugins/blob/c0346dcdb16ae9f136b3abf83efb6458191f9300/bt-daemon/src/translate/claude.rs#L1173-L1300 * https://github.com/braintrustdata/braintrust-spec/blob/b068e39112e081e45b6070e035877f1e2e83f9b7/skills/instrumentation-spec/references/features/eval-spans.md#trace-shape * - * An LLM span starts where the previous span ended, since the agent only + * An LLM span starts where the latest earlier span ended, since the agent only * writes a request's timestamps once its blocks finish generating. Braintrust's * Claude Code translator starts it at the first block instead, which leaves the * generation time as a gap. Their Codex translator does what we do.