Skip to content

Commit e563ea7

Browse files
authored
fix(flue): Fix span nesting (#2098)
- Nests spans created inside workflow under workflow spans - Nests spans created inside tools under tool span <img width="273" height="415" alt="Screenshot 2026-06-12 at 11 23 09" src="https://github.com/user-attachments/assets/88275889-6746-4fa6-bd2c-30aace861e34" />
1 parent c14f9f6 commit e563ea7

11 files changed

Lines changed: 337 additions & 14 deletions

.changeset/light-days-brush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"braintrust": patch
3+
---
4+
5+
fix(flue): Fix span nesting

e2e/scenarios/flue-instrumentation/.flue/workflows/instrumentation.mjs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createAgent, Type } from "@flue/runtime";
22
import { local } from "@flue/runtime/node";
3+
import { traced } from "braintrust";
34
import {
45
FLUE_MODEL,
56
FLUE_REASONING_MODEL,
@@ -48,12 +49,27 @@ const flueE2EAgent = createAgent(() => ({
4849
const lookupTool = {
4950
description:
5051
"Return a deterministic lookup result with an id needed by web_search.",
51-
execute: async (args) =>
52-
JSON.stringify({
52+
execute: async (args) => {
53+
await traced(
54+
async (span) => {
55+
span.log({ output: "lookup-active" });
56+
},
57+
{
58+
name: "flue.toolCurrentProbe",
59+
event: {
60+
metadata: {
61+
scenario: SCENARIO_NAME,
62+
},
63+
},
64+
},
65+
);
66+
67+
return JSON.stringify({
5368
id: "flue-session-2026",
5469
query: args.query,
5570
topic: "session instrumentation",
56-
}),
71+
});
72+
},
5773
name: "lookup",
5874
parameters: Type.Object({
5975
query: Type.String(),
@@ -101,6 +117,20 @@ export async function route(_ctx, next) {
101117
}
102118

103119
export async function run({ init, payload }) {
120+
await traced(
121+
async (span) => {
122+
span.log({ output: "active" });
123+
},
124+
{
125+
name: "flue.workflowCurrentProbe",
126+
event: {
127+
metadata: {
128+
scenario: SCENARIO_NAME,
129+
},
130+
},
131+
},
132+
);
133+
104134
const harness = await init(flueE2EAgent, { name: "default" });
105135
const session = await harness.session("main");
106136
const skillSession = await harness.session("skill");

e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-auto-hook.span-tree.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
"name": "workflow:instrumentation",
55
"type": "task",
66
"children": [
7+
{
8+
"name": "flue.workflowCurrentProbe",
9+
"children": [],
10+
"output": "active",
11+
"metadata": {
12+
"scenario": "flue-instrumentation"
13+
}
14+
},
715
{
816
"name": "flue.prompt",
917
"type": "task",
@@ -68,7 +76,16 @@
6876
{
6977
"name": "tool:lookup",
7078
"type": "tool",
71-
"children": [],
79+
"children": [
80+
{
81+
"name": "flue.toolCurrentProbe",
82+
"children": [],
83+
"output": "lookup-active",
84+
"metadata": {
85+
"scenario": "flue-instrumentation"
86+
}
87+
}
88+
],
7289
"input": {
7390
"query": "flue instrumentation"
7491
},

e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-auto-hook.span-tree.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ span_tree:
1919
metrics: {
2020
"duration_ms": 0
2121
}
22+
├── flue.workflowCurrentProbe
23+
│ output: "active"
24+
│ metadata: {
25+
│ "scenario": "flue-instrumentation"
26+
│ }
2227
├── flue.prompt [task]
2328
│ input: [
2429
│ {
@@ -116,6 +121,11 @@ span_tree:
116121
│ │ metrics: {
117122
│ │ "duration_ms": 0
118123
│ │ }
124+
│ │ └── flue.toolCurrentProbe
125+
│ │ output: "lookup-active"
126+
│ │ metadata: {
127+
│ │ "scenario": "flue-instrumentation"
128+
│ │ }
119129
│ ├── llm:claude-sonnet-4-5-20250929 [llm]
120130
│ │ input: [
121131
│ │ {

e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-cli.span-tree.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
"name": "workflow:instrumentation",
55
"type": "task",
66
"children": [
7+
{
8+
"name": "flue.workflowCurrentProbe",
9+
"children": [],
10+
"output": "active",
11+
"metadata": {
12+
"scenario": "flue-instrumentation"
13+
}
14+
},
715
{
816
"name": "flue.prompt",
917
"type": "task",
@@ -68,7 +76,16 @@
6876
{
6977
"name": "tool:lookup",
7078
"type": "tool",
71-
"children": [],
79+
"children": [
80+
{
81+
"name": "flue.toolCurrentProbe",
82+
"children": [],
83+
"output": "lookup-active",
84+
"metadata": {
85+
"scenario": "flue-instrumentation"
86+
}
87+
}
88+
],
7289
"input": {
7390
"query": "flue instrumentation"
7491
},

e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-cli.span-tree.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ span_tree:
1919
metrics: {
2020
"duration_ms": 0
2121
}
22+
├── flue.workflowCurrentProbe
23+
│ output: "active"
24+
│ metadata: {
25+
│ "scenario": "flue-instrumentation"
26+
│ }
2227
├── flue.prompt [task]
2328
│ input: [
2429
│ {
@@ -116,6 +121,11 @@ span_tree:
116121
│ │ metrics: {
117122
│ │ "duration_ms": 0
118123
│ │ }
124+
│ │ └── flue.toolCurrentProbe
125+
│ │ output: "lookup-active"
126+
│ │ metadata: {
127+
│ │ "scenario": "flue-instrumentation"
128+
│ │ }
119129
│ ├── llm:claude-sonnet-4-5-20250929 [llm]
120130
│ │ input: [
121131
│ │ {

e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-explicit.span-tree.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
"name": "workflow:instrumentation",
55
"type": "task",
66
"children": [
7+
{
8+
"name": "flue.workflowCurrentProbe",
9+
"children": [],
10+
"output": "active",
11+
"metadata": {
12+
"scenario": "flue-instrumentation"
13+
}
14+
},
715
{
816
"name": "flue.prompt",
917
"type": "task",
@@ -68,7 +76,16 @@
6876
{
6977
"name": "tool:lookup",
7078
"type": "tool",
71-
"children": [],
79+
"children": [
80+
{
81+
"name": "flue.toolCurrentProbe",
82+
"children": [],
83+
"output": "lookup-active",
84+
"metadata": {
85+
"scenario": "flue-instrumentation"
86+
}
87+
}
88+
],
7289
"input": {
7390
"query": "flue instrumentation"
7491
},

e2e/scenarios/flue-instrumentation/__snapshots__/flue-v0-8-0-explicit.span-tree.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ span_tree:
1919
metrics: {
2020
"duration_ms": 0
2121
}
22+
├── flue.workflowCurrentProbe
23+
│ output: "active"
24+
│ metadata: {
25+
│ "scenario": "flue-instrumentation"
26+
│ }
2227
├── flue.prompt [task]
2328
│ input: [
2429
│ {
@@ -116,6 +121,11 @@ span_tree:
116121
│ │ metrics: {
117122
│ │ "duration_ms": 0
118123
│ │ }
124+
│ │ └── flue.toolCurrentProbe
125+
│ │ output: "lookup-active"
126+
│ │ metadata: {
127+
│ │ "scenario": "flue-instrumentation"
128+
│ │ }
119129
│ ├── llm:claude-sonnet-4-5-20250929 [llm]
120130
│ │ input: [
121131
│ │ {

e2e/scenarios/flue-instrumentation/assertions.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,19 +160,26 @@ function isFlueChildSpan(event: CapturedLogEvent): boolean {
160160
event.span.name?.startsWith("tool:") === true ||
161161
event.span.name?.startsWith("task:") === true ||
162162
event.span.name?.startsWith("compaction:") === true ||
163+
event.span.name === "flue.toolCurrentProbe" ||
163164
event.span.name === "flue.task"
164165
);
165166
}
166167

167168
function buildSpanTree(events: CapturedLogEvent[]): SpanTreeEntry[] {
168169
const workflow = findLatestSpanByPrefix(events, "workflow:");
170+
const workflowCurrentProbe = findLatestChildSpan(
171+
events,
172+
"flue.workflowCurrentProbe",
173+
workflow?.span.id,
174+
);
169175
const promptSpan = findFlueOperation(events, "flue.prompt");
170176
const skillSpan = findFlueOperation(events, "flue.skill");
171177
const taskSpan = findFlueOperation(events, "flue.task");
172178
const compactSpan = findFlueOperation(events, "flue.compact");
173179

174180
return [
175181
workflow,
182+
workflowCurrentProbe,
176183
promptSpan,
177184
...findFlueDescendants(events, promptSpan, isFlueChildSpan),
178185
skillSpan,
@@ -223,6 +230,41 @@ export function defineFlueInstrumentationAssertions(options: {
223230
});
224231
});
225232

233+
test(
234+
"makes the Flue workflow span current for app spans",
235+
testConfig,
236+
() => {
237+
const workflow = findLatestSpanByPrefix(events, "workflow:");
238+
const appSpan = findLatestChildSpan(
239+
events,
240+
"flue.workflowCurrentProbe",
241+
workflow?.span.id,
242+
);
243+
244+
expect(appSpan).toBeDefined();
245+
expect(appSpan?.span.parentIds).toEqual([workflow?.span.id]);
246+
expect(appSpan?.output).toBe("active");
247+
},
248+
);
249+
250+
test("makes Flue tool spans current for app spans", testConfig, () => {
251+
const promptSpan = findFlueOperation(events, "flue.prompt");
252+
const lookupToolSpan = findFlueDescendants(
253+
events,
254+
promptSpan,
255+
(event) => event.span.name === "tool:lookup",
256+
)[0];
257+
const appSpan = findLatestChildSpan(
258+
events,
259+
"flue.toolCurrentProbe",
260+
lookupToolSpan?.span.id,
261+
);
262+
263+
expect(appSpan).toBeDefined();
264+
expect(appSpan?.span.parentIds).toEqual([lookupToolSpan?.span.id]);
265+
expect(appSpan?.output).toBe("lookup-active");
266+
});
267+
226268
test("captures observe-based Flue operation spans", testConfig, () => {
227269
for (const flueSpanName of [
228270
"flue.prompt",

0 commit comments

Comments
 (0)