Skip to content
Open
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
30 changes: 15 additions & 15 deletions bt-daemon/src/translate/cursor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1138,26 +1138,21 @@ impl CursorTranslator {
}
self.ensure_turn(e.ts_ms, None, None, "hook_order", ops);
let spawn = hook.tool_call_id.as_deref();
let Some((parent, turn)) = spawn
.and_then(|s| self.tools.get(s).or_else(|| self.completed.get(s)))
.map(|t| (t.row.span_id.clone(), t.turn.clone()))
.or_else(|| {
self.turn
.as_ref()
.map(|turn| (turn.id.clone(), turn.id.clone()))
})
let Some(turn) = spawn
.and_then(|call| self.tools.get(call).or_else(|| self.completed.get(call)))
.map(|tool| tool.turn.clone())
.or_else(|| self.turn.as_ref().map(|turn| turn.id.clone()))
else {
return;
};
let mut row = self.row(
ids::span_id(&self.namespace, &format!("subagent:{id}")),
parent,
turn,
"Cursor subagent",
SpanType::Task,
);
row.turn_span_id = Some(turn);
row.start_ms = Some(e.ts_ms);
row.input = Some(json!(hook.task));
row.input = Some(user_prompt(&hook.task));
row.metadata = Some(
json!({"subagent_id":id,"spawning_tool_call_id":spawn,"model":hook.subagent_model,"subagent_type":hook.subagent_type,"recursive_activity_verified":false}),
);
Expand All @@ -1182,9 +1177,10 @@ impl CursorTranslator {
return None;
}
let mut matching_ids = self.subagents.iter().filter_map(|(id, row)| {
let task_matches = hook.task.as_ref().is_none_or(|task| {
row.input.as_ref().and_then(Value::as_str) == Some(task.as_str())
});
let task_matches = hook
.task
.as_ref()
.is_none_or(|task| row.input.as_ref() == Some(&user_prompt(task)));
let type_matches = hook.subagent_type.as_ref().is_none_or(|kind| {
row.metadata
.as_ref()
Expand Down Expand Up @@ -1248,7 +1244,7 @@ impl CursorTranslator {
let duration = hook.duration_ms.and_then(|ms| i64::try_from(ms).ok());
row.start_ms = Some(duration.map_or(e.ts_ms, |duration| e.ts_ms.saturating_sub(duration)));
row.end_ms = Some(e.ts_ms);
row.input = hook.task.as_ref().map(|task| json!(task));
row.input = hook.task.as_ref().map(|task| user_prompt(task));
row.output = hook.summary.as_ref().map(|summary| json!(summary));
row.metadata = Some(json!({
"subagent_id": hook.subagent_id,
Expand Down Expand Up @@ -1600,6 +1596,10 @@ impl AgentTranslator for CursorTranslator {
Ok(ops)
}
}
fn user_prompt(text: &str) -> Value {
json!([{ "role": "user", "content": text }])
}

fn assistant_message(content: &[Value]) -> Value {
let mut text = String::new();
let mut reasoning = Vec::new();
Expand Down
132 changes: 62 additions & 70 deletions bt-daemon/tests/cursor_translator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1069,7 +1069,7 @@ fn synthetic_usage_without_verified_prompt_generation_stays_unavailable() {
}

#[test]
fn synthetic_subagents_parent_to_spawning_tool_and_unmatched_stop_stays_incomplete() {
fn synthetic_subagents_stay_on_the_spawning_turn() {
let mut h = Harness::new("test-session");
h.handle(&event(
"beforeSubmitPrompt",
Expand All @@ -1091,95 +1091,87 @@ fn synthetic_subagents_parent_to_spawning_tool_and_unmatched_stop_stays_incomple
130,
json!({"subagent_id":"child-b","tool_call_id":"spawn","subagent_type":"explore","subagent_model":"auto","task":"explore"}),
));
h.handle(&event(
"subagentStart",
135,
json!({"subagent_id":"child-c","tool_call_id":"spawn","subagent_type":"explore","subagent_model":"auto","task":"summarize"}),
));
h.handle(&event(
"subagentStop",
140,
json!({"subagent_id":"child-a","subagent_type":"explore","status":"completed","summary":"found the issue"}),
));
h.handle(&event(
"subagentStop",
145,
json!({"task":"summarize","subagent_type":"explore","status":"completed","summary":"summary done"}),
));
h.handle(&event(
"subagentStop",
150,
json!({
"subagent_id":"child-terminal-only",
"status":"completed",
"summary":"finished work",
"subagent_type":"explore"
}),
));
h.handle(&event(
"sessionEnd",
160,
json!({"reason":"completed","final_status":"completed"}),
));
h.finish();
let turns = h.turns();
let turn_id = turns[0].span_id.clone();
let root_id = turns[0].root_span_id.clone();
let rows = h.rows();
let children: Vec<_> = h
.inserted(SpanType::Task)
.into_iter()
.filter(|r| r.name == "Cursor subagent")
.collect();
assert_eq!(children.len(), 2);
let rows = h.rows();
let spawn_tool_id = h
.inserted(SpanType::Tool)
.into_iter()
.find(|row| row.name == "Task")
.unwrap()
.span_id
.clone();
assert_eq!(children.len(), 4);
for child in children {
assert_eq!(child.parent_span_ids, vec![spawn_tool_id.clone()]);
assert_eq!(child.turn_span_id, Some(h.turns()[0].span_id.clone()));
assert_eq!(
child.metadata.as_ref().unwrap()["spawning_tool_call_id"],
"spawn"
);
assert_eq!(
child.metadata.as_ref().unwrap()["recursive_activity_verified"],
false
);
assert_eq!(child.root_span_id, root_id);
assert_eq!(child.parent_span_ids, vec![turn_id.clone()]);
assert_eq!(child.metadata.as_ref().unwrap()["turn_span_id"], turn_id);
let row = &rows[&child.span_id];
if child.start_ms == Some(120) {
assert_eq!(row["end_ms"], 140);
assert_eq!(child.input, Some(json!("inspect the code")));
assert_eq!(row["output"], "found the issue");
assert_eq!(row["metadata"]["status"], "completed");
} else {
assert!(row.get("output").is_none());
assert_eq!(row["metadata"]["status"], "incomplete");
match row["metadata"]["subagent_id"].as_str() {
Some("child-a") => {
assert_eq!(row["end_ms"], 140);
assert_eq!(
child.input,
Some(json!([{ "role": "user", "content": "inspect the code" }]))
);
assert_eq!(row["output"], "found the issue");
assert_eq!(row["metadata"]["status"], "completed");
assert_eq!(row["metadata"]["spawning_tool_call_id"], "spawn");
assert_eq!(row["metadata"]["recursive_activity_verified"], false);
}
Some("child-b") => {
assert!(row.get("output").is_none());
assert_eq!(row["metadata"]["status"], "incomplete");
}
Some("child-c") => {
assert_eq!(row["output"], "summary done");
assert_eq!(row["metadata"]["status"], "completed");
assert!(row["metadata"].get("result_completeness").is_none());
}
Some("child-terminal-only") => {
assert_eq!(child.end_ms, Some(150));
assert_eq!(row["output"], "finished work");
assert_eq!(row["metadata"]["status"], "completed");
assert_eq!(row["metadata"]["start_time_estimated"], true);
assert_eq!(
row["metadata"]["result_completeness"],
"terminal_observation_only"
);
assert!(row["metadata"].get("model").is_none());
}
other => panic!("unexpected subagent {other:?}"),
}
}
assert_eq!(
rows[&h.inserted(SpanType::Tool)[0].span_id]["metadata"]["status"],
"incomplete"
);
}

#[test]
fn terminal_only_subagent_stop_synthesizes_estimated_span() {
let mut h = Harness::new("test-session");
h.handle(&event(
"beforeSubmitPrompt",
100,
json!({"generation_id":"t","prompt":"delegate"}),
));
h.handle(&event(
"subagentStop",
140,
json!({
"subagent_id":"child-terminal-only",
"status":"completed",
"summary":"finished work",
"subagent_type":"explore"
}),
));
h.finish();

let child = h
.inserted(SpanType::Task)
.into_iter()
.find(|row| row.name == "Cursor subagent")
.unwrap();
assert_eq!(child.start_ms, Some(140));
assert_eq!(child.end_ms, Some(140));
assert_eq!(child.output, Some(json!("finished work")));
let metadata = child.metadata.as_ref().unwrap();
assert_eq!(metadata["start_time_estimated"], true);
assert_eq!(metadata["result_completeness"], "terminal_observation_only");
assert_eq!(metadata["status"], "completed");
assert_eq!(metadata["subagent_type"], "explore");
// subagentStop does not report the subagent's model.
assert!(metadata.get("model").is_none());
assert_eq!(child.parent_span_ids, vec![h.turns()[0].span_id.clone()]);
}
#[test]
fn synthetic_compaction_is_observation_only_and_clears_unavailable_context() {
Expand Down
Loading