Skip to content
Merged
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
133 changes: 133 additions & 0 deletions crates/core/tests/unit/observability/atof_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,37 @@ fn wire_format_llm_event(
))
}

fn openclaw_agent_scope_event(
uuid: Uuid,
parent_uuid: Option<Uuid>,
scope_category: ScopeCategory,
name: &str,
session_id: &str,
scope_role: Option<&str>,
) -> Event {
let mut metadata = Map::new();
metadata.insert("source".to_string(), json!("openclaw.session_start"));
metadata.insert("hook_event_name".to_string(), json!("session_start"));
metadata.insert("session_id".to_string(), json!(session_id));
if let Some(scope_role) = scope_role {
metadata.insert("nemo_relay_scope_role".to_string(), json!(scope_role));
}

Event::Scope(ScopeEvent::new(
BaseEvent::builder()
.uuid(uuid)
.parent_uuid_opt(parent_uuid)
.name(name)
.data(json!({"session_id": session_id}))
.metadata(serde_json::Value::Object(metadata))
.build(),
scope_category,
Vec::new(),
EventCategory::agent(),
None,
))
}

fn read_jsonl(path: &Path) -> Vec<serde_json::Value> {
fs::read_to_string(path)
.unwrap()
Expand Down Expand Up @@ -437,6 +468,108 @@ fn subscriber_preserves_wire_format_llm_lifecycle_payloads_as_raw_jsonl() {
assert_eq!(lines[5]["data"]["usage"]["cost_usd"], 0.001);
}

#[test]
fn openclaw_subagent_events_preserve_nested_and_fallback_parent_uuid() {
let dir = temp_dir("atof-openclaw-subagent-parentage");
let exporter = AtofExporter::new(
AtofExporterConfig::new()
.with_output_directory(&dir)
.with_filename("events.jsonl"),
)
.unwrap();
let subscriber = exporter.subscriber();
let parent_uuid = Uuid::now_v7();
let nested_child_uuid = Uuid::now_v7();
let fallback_child_uuid = Uuid::now_v7();

let events = [
openclaw_agent_scope_event(
parent_uuid,
None,
ScopeCategory::Start,
"requester-agent",
"parent-session",
None,
),
openclaw_agent_scope_event(
nested_child_uuid,
Some(parent_uuid),
ScopeCategory::Start,
"nested-worker",
"nested-child-session",
Some("subagent"),
),
openclaw_agent_scope_event(
nested_child_uuid,
Some(parent_uuid),
ScopeCategory::End,
"nested-worker",
"nested-child-session",
Some("subagent"),
),
openclaw_agent_scope_event(
parent_uuid,
None,
ScopeCategory::End,
"requester-agent",
"parent-session",
None,
),
openclaw_agent_scope_event(
fallback_child_uuid,
None,
ScopeCategory::Start,
"fallback-worker",
"fallback-child-session",
Some("subagent"),
),
openclaw_agent_scope_event(
fallback_child_uuid,
None,
ScopeCategory::End,
"fallback-worker",
"fallback-child-session",
Some("subagent"),
),
];

for event in &events {
subscriber(event);
}
exporter.force_flush().unwrap();

let lines = read_jsonl(exporter.path());
let nested_start = lines
.iter()
.find(|line| {
line["uuid"] == nested_child_uuid.to_string() && line["scope_category"] == "start"
})
.unwrap();
assert_eq!(nested_start["parent_uuid"], parent_uuid.to_string());
assert_eq!(
nested_start["metadata"]["nemo_relay_scope_role"],
json!("subagent")
);

let fallback_start = lines
.iter()
.find(|line| {
line["uuid"] == fallback_child_uuid.to_string() && line["scope_category"] == "start"
})
.unwrap();
assert!(
!fallback_start
.as_object()
.unwrap()
.contains_key("parent_uuid")
|| fallback_start["parent_uuid"].is_null()
);
assert_eq!(
fallback_start["metadata"]["nemo_relay_scope_role"],
json!("subagent")
);
}

#[test]
fn register_deregister_flush_and_shutdown_work_with_runtime_events() {
let _guard = crate::observability::test_mutex().lock().unwrap();
Expand Down
123 changes: 123 additions & 0 deletions crates/core/tests/unit/observability/openinference_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,129 @@ fn openclaw_replay_payloads_emit_flattened_openinference_llm_attributes() {
assert_no_attr_contains(&attributes, "secret-token");
}

#[test]
fn openclaw_subagent_scopes_preserve_nested_and_fallback_parent_linkage() {
let (provider, exporter) = make_provider();
let mut processor =
OpenInferenceEventProcessor::new(provider.clone(), "test-scope".to_string());
let parent_uuid = Uuid::now_v7();
let nested_child_uuid = Uuid::now_v7();
let fallback_child_uuid = Uuid::now_v7();

let nested_parent_start = Event::Scope(ScopeEvent::new(
BaseEvent::builder()
.uuid(parent_uuid)
.name("requester-agent")
.metadata(json!({
"source": "openclaw.session_start",
"hook_event_name": "session_start",
"session_id": "parent-session"
}))
.build(),
ScopeCategory::Start,
Vec::new(),
EventCategory::agent(),
None,
));
let nested_child_start = Event::Scope(ScopeEvent::new(
BaseEvent::builder()
.uuid(nested_child_uuid)
.parent_uuid(parent_uuid)
.name("nested-worker")
.metadata(json!({
"source": "openclaw.session_start",
"hook_event_name": "session_start",
"session_id": "nested-child-session",
"nemo_relay_scope_role": "subagent"
}))
.build(),
ScopeCategory::Start,
Vec::new(),
EventCategory::agent(),
None,
));
let nested_child_end = Event::Scope(ScopeEvent::new(
BaseEvent::builder()
.uuid(nested_child_uuid)
.parent_uuid(parent_uuid)
.name("nested-worker")
.build(),
ScopeCategory::End,
Vec::new(),
EventCategory::agent(),
None,
));
let nested_parent_end = Event::Scope(ScopeEvent::new(
BaseEvent::builder()
.uuid(parent_uuid)
.name("requester-agent")
.build(),
ScopeCategory::End,
Vec::new(),
EventCategory::agent(),
None,
));
let fallback_child_start = Event::Scope(ScopeEvent::new(
BaseEvent::builder()
.uuid(fallback_child_uuid)
.name("fallback-worker")
.metadata(json!({
"source": "openclaw.session_start",
"hook_event_name": "session_start",
"session_id": "fallback-child-session",
"nemo_relay_scope_role": "subagent"
}))
.build(),
ScopeCategory::Start,
Vec::new(),
EventCategory::agent(),
None,
));
let fallback_child_end = Event::Scope(ScopeEvent::new(
BaseEvent::builder()
.uuid(fallback_child_uuid)
.name("fallback-worker")
.build(),
ScopeCategory::End,
Vec::new(),
EventCategory::agent(),
None,
));

for event in [
nested_parent_start,
nested_child_start,
nested_child_end,
nested_parent_end,
fallback_child_start,
fallback_child_end,
] {
processor.process(&event);
}
processor.force_flush().unwrap();

let spans = exporter.get_finished_spans().unwrap();
let nested_child_span = spans
.iter()
.find(|span| span.name.as_ref() == "nested-worker")
.unwrap();
let fallback_child_span = spans
.iter()
.find(|span| span.name.as_ref() == "fallback-worker")
.unwrap();
let nested_child_attributes = attr_map(&nested_child_span.attributes);
let fallback_child_attributes = attr_map(&fallback_child_span.attributes);

assert_eq!(
nested_child_attributes.get("nemo_relay.parent_uuid"),
Some(&parent_uuid.to_string())
);
assert_eq!(
fallback_child_attributes.get("nemo_relay.parent_uuid"),
Some(&String::new())
);
}

#[test]
fn generic_unannotated_llm_output_does_not_emit_flattened_output_message_attrs() {
let (provider, exporter) = make_provider();
Expand Down
Loading
Loading