fix(lens): preserve SDK actors and bound native identity size

This commit is contained in:
moe-berri 2026-10-06 12:45:28 -07:00
parent a685616392
commit 01fcf24afd
6 changed files with 124 additions and 49 deletions

View file

@ -1,9 +1,12 @@
WITH
(Framework IN ('claude-code', 'claude-agent-sdk') AND (SpanAttributes['session.id'] != '' OR SpanAttributes['agent_id'] != '')) AS native,
coalesce(nullIf(SpanAttributes['query_source_safe'], ''), SpanAttributes['query_source']) AS native_source,
((Framework = 'claude-code' OR (Framework = 'claude-agent-sdk' AND
(startsWith(SpanName, 'claude_code.') OR SpanAttributes['agent_id'] != '' OR native_source != '')))
AND (SpanAttributes['session.id'] != '' OR SpanAttributes['agent_id'] != '')) AS native,
if(SpanAttributes['session.id'] = '', TraceId, SpanAttributes['session.id']) AS native_session,
(native AND SpanAttributes['agent_id'] != '') AS native_child,
(native AND NOT native_child AND (SpanName = 'claude_code.interaction' OR
coalesce(nullIf(SpanAttributes['query_source_safe'], ''), SpanAttributes['query_source']) IN
native_source IN
('repl_main_thread', 'sdk', 'sdk_main_thread', 'generate_session_title', 'prompt_suggestion'))) AS native_root,
page AS (
SELECT TraceId AS trace_id,
@ -45,11 +48,12 @@ LEFT JOIN (
arraySort(groupUniqArrayIf(toString(Framework), Framework != '')) AS frameworks,
groupUniqArrayIf(native_session, native_root) AS root_sessions,
groupUniqArrayIf((native_session, SpanId), native_root AND SpanName = 'claude_code.interaction') AS root_interactions,
uniqExactIf((native_session, SpanAttributes['agent_id']), native_child)
+ length(root_sessions) AS native_count,
groupUniqArrayIf((native_session, SpanAttributes['agent_id']), native_child) AS child_identities,
groupUniqArrayIf((native_session, SpanAttributes['agent_id'], SpanId), native_child AND SpanName = 'claude_code.interaction') AS child_interactions,
length(child_identities) + length(root_sessions) AS native_count,
native_count + uniqExactIf(if(AgentName = '', SpanName, AgentName), ObservationType = 'agent' AND NOT native) AS agent_count,
uniqExactIf(SpanId, ObservationType = 'agent' AND NOT native)
+ uniqExactIf((native_session, SpanAttributes['agent_id']), native_child)
+ arraySum(arrayMap(child -> greatest(1, arrayCount(interaction -> (interaction.1, interaction.2) = child, child_interactions)), child_identities))
+ arraySum(arrayMap(session -> greatest(1, arrayCount(interaction -> interaction.1 = session, root_interactions)), root_sessions)) AS resolved_invocations
FROM otel_traces
WHERE Timestamp >= (SELECT min(trace_start) FROM page)

View file

@ -841,6 +841,20 @@ async fn native_list_counts_each_root_session_and_does_not_count_children_as_roo
"agent",
"claude_code.interaction",
),
(
"child-resumed",
"session-a",
"child",
"agent",
"claude_code.interaction",
),
(
"legacy-sdk",
"sdk-session",
"",
"agent",
"ClaudeAgentSDK.query",
),
];
insert_rows(client, &writer, DATABASE, InsertTable::OtelTraces, records.into_iter().map(|(id, session, actor, kind, name)| BTreeMap::from([
("Timestamp".into(), json!(1_790_000_000_000_000_000_i64)),
@ -848,9 +862,9 @@ async fn native_list_counts_each_root_session_and_does_not_count_children_as_roo
("SpanId".into(), json!(id)),
("SpanName".into(), json!(name)),
("ObservationType".into(), json!(kind)),
("Framework".into(), json!(framework)),
("Framework".into(), json!(if id == "legacy-sdk" { "claude-agent-sdk" } else { framework })),
("AgentName".into(), json!("assistant")),
("SpanAttributes".into(), json!({"session.id": session, "agent_id": actor, "query_source_safe": "sdk"})),
("SpanAttributes".into(), json!({"session.id": session, "agent_id": actor, "query_source_safe": if id == "legacy-sdk" { "" } else { "sdk" }})),
("TeamId".into(), json!("team-a")),
("ApiKeyHash".into(), json!("key-a")),
])).collect()).await?;
@ -879,10 +893,10 @@ async fn native_list_counts_each_root_session_and_does_not_count_children_as_roo
.get_trace(&store, &access, "mixed-sessions", &summary.trace_ref)
.await?
.ok_or("missing trace")?;
assert_eq!((summary.agent_count, summary.agent_invocations), (3, 4));
assert_eq!((summary.agent_count, summary.agent_invocations), (4, 6));
assert_eq!(
(detail.summary.agent_count, detail.summary.agent_invocations),
(3, 4)
(4, 6)
);
Ok(())
}

View file

@ -1,11 +1,17 @@
use std::collections::{BTreeMap, BTreeSet, HashMap};
use sha2::{Digest, Sha256};
use crate::query::named::TraceSpansRow;
use super::graph::Graph;
pub(super) fn native(row: &TraceSpansRow) -> bool {
matches!(row.framework.as_str(), "claude-code" | "claude-agent-sdk")
(row.framework == "claude-code"
|| (row.framework == "claude-agent-sdk"
&& (row.name.starts_with("claude_code.")
|| !row.native_agent_id.is_empty()
|| !row.query_source.is_empty())))
&& (!row.session_id.is_empty() || !row.native_agent_id.is_empty())
}
@ -15,7 +21,15 @@ fn identity(row: &TraceSpansRow, agent: &str) -> String {
} else {
&row.session_id
};
format!("claude-code:{}:{session}:{agent}", session.len())
let mut digest = Sha256::new();
digest.update((session.len() as u64).to_be_bytes());
digest.update(session);
digest.update(agent);
format!("claude-code:{:x}", digest.finalize())
}
fn label(value: &str) -> String {
value.chars().take(256).collect()
}
fn root(row: &TraceSpansRow) -> String {
@ -112,7 +126,7 @@ impl Actors {
id: id.clone(),
parent: (id != &root(row)).then(|| root(row)),
name: if id == &root(row) {
row.agent.clone()
label(&row.agent)
} else {
"Subagent".to_owned()
},
@ -138,7 +152,7 @@ impl Actors {
if let Some((_, name)) = name
&& !name.is_empty()
{
actor.name = name.to_owned();
actor.name = label(name);
}
}
}

View file

@ -1700,3 +1700,45 @@ fn unresolved_native_actors_are_distinguished_from_legacy_grouping(
assert_eq!(trace.spans[0].actor_id, None);
assert_eq!(trace.spans[0].actor_unassigned, unassigned);
}
#[rstest]
fn openinference_sdk_session_keeps_recorded_root_ownership() {
let root = TraceSpansRow {
framework: "claude-agent-sdk".into(),
session_id: "sdk-session".into(),
..row("root", "", "ClaudeAgentSDK.query", "agent", "sdk-reader")
};
let trace = resolve_trace("trace", "ref", &[root], &[]).unwrap();
assert_eq!(trace.summary.agent_count, 1);
assert_eq!(trace.agents[0].name, "sdk-reader");
assert!(!trace.spans[0].actor_unassigned);
}
#[rstest]
fn inherited_native_actor_identity_and_labels_have_bounded_size() {
let root = TraceSpansRow {
framework: "claude-code".into(),
session_id: "s".into(),
native_agent_id: "a".repeat(65_536),
query_source: format!("agent.custom.{}", "n".repeat(65_536)),
..row("root", "", "claude_code.interaction", "agent", "assistant")
};
let children = (0..1_024).map(|index| TraceSpansRow {
framework: "claude-code".into(),
session_id: "s".into(),
..row(
&format!("child-{index}"),
"root",
"Read",
"tool",
"assistant",
)
});
let rows: Vec<_> = std::iter::once(root).chain(children).collect();
let trace = resolve_trace("trace", "ref", &rows, &[]).unwrap();
assert_eq!(trace.summary.agent_count, 1);
assert!(trace.spans.iter().all(|span| {
span.actor_id.as_ref().is_some_and(|id| id.len() <= 80) && span.agent.len() <= 1_024
}));
assert!(serde_json::to_vec(&trace).unwrap().len() < 3_000_000);
}

View file

@ -616,41 +616,42 @@ describe("coding sessions", () => {
});
});
it("keeps explicit native actors separate across workflow branches and background resumes", () => {
const span = (id: string, actor: string, parentActor: string | null, offset: number): Span => ({
...root,
span_id: id,
name: "claude_code.llm_request",
type: "llm",
framework: "claude-code",
parent_span_id: "root",
actor_id: actor,
parent_actor_id: parentActor,
agent: "general-purpose",
start_offset_ms: offset,
});
const spans = [
{ ...root, framework: "claude-code", actor_id: "root-actor", parent_actor_id: null, agent: "claude-code" },
span("first", "actor-one", "root-actor", 1),
span("second", "actor-two", "root-actor", 2),
span("nested", "actor-three", "actor-one", 3),
{ ...span("resumed", "actor-one", "root-actor", 4), parent_span_id: null },
];
const details = new Map(spans.map((span) => [span.span_id, detail(span.span_id, "", `Reply ${span.span_id}`)]));
const items = buildConversation(spans, details, true);
expect(items.filter((item) => item.span.span_id === "resumed").map((item) => item.agentId)).toEqual(["actor-one"]);
const groups = groupConversation(items, spans);
const branches = groups.filter((group) => group.kind === "branch");
expect(branches.map((group) => group.id)).toEqual(["actor-one", "actor-two"]);
expect(branches[0].children.map((group) => (group.kind === "branch" ? group.id : group.item.span.span_id))).toEqual([
"first",
"actor-three",
"resumed",
]);
expect(
items.filter((item) => ["first", "resumed"].includes(item.span.span_id)).map((item) => item.agentName),
).toEqual(["general-purpose (1)", "general-purpose (1)"]);
});
it.each(["claude-code", "claude-agent-sdk"])(
"keeps explicit %s actors separate across workflow branches and background resumes",
(framework) => {
const span = (id: string, actor: string, parentActor: string | null, offset: number): Span => ({
...root,
span_id: id,
name: "claude_code.llm_request",
type: "llm",
framework,
parent_span_id: "root",
actor_id: actor,
parent_actor_id: parentActor,
agent: "general-purpose",
start_offset_ms: offset,
});
const spans = [
{ ...root, framework, actor_id: "root-actor", parent_actor_id: null, agent: "claude-code" },
span("first", "actor-one", "root-actor", 1),
span("second", "actor-two", "root-actor", 2),
span("nested", "actor-three", "actor-one", 3),
{ ...span("resumed", "actor-one", "root-actor", 4), parent_span_id: null },
];
const details = new Map(spans.map((span) => [span.span_id, detail(span.span_id, "", `Reply ${span.span_id}`)]));
const items = buildConversation(spans, details, true);
expect(items.filter((item) => item.span.span_id === "resumed").map((item) => item.agentId)).toEqual(["actor-one"]);
const groups = groupConversation(items, spans);
const branches = groups.filter((group) => group.kind === "branch");
expect(branches.map((group) => group.id)).toEqual(["actor-one", "actor-two"]);
expect(branches[0].children.map((group) => (group.kind === "branch" ? group.id : group.item.span.span_id))).toEqual(
["first", "actor-three", "resumed"],
);
expect(
items.filter((item) => ["first", "resumed"].includes(item.span.span_id)).map((item) => item.agentName),
).toEqual(["general-purpose (1)", "general-purpose (1)"]);
},
);
it.each([false, true])("preserves legacy history while isolating unconfirmed actors (%s)", (unassigned) => {
const spans: Span[] = [

View file

@ -239,7 +239,7 @@ function agentLabels(agents: readonly Span[], details: ReadonlyMap<string, SpanD
const args = detail && isNativeAgent(agent) ? toolInput(detail.input, detail.input_ui) : undefined;
const description = args && typeof args === "object" && "description" in args ? args.description : undefined;
const name =
agent.framework === "claude-code" && (agent.actor_id || !isNativeAgent(agent))
agent.actor_id || (agent.framework === "claude-code" && !isNativeAgent(agent))
? agent.agent || agent.name || "Agent"
: agent.name || agent.agent || "Agent";
return { agent, name: typeof description === "string" && description ? description : name };