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
19 changes: 19 additions & 0 deletions bt-daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,25 @@ run`, from that invocation's settings. An explicit `--additional-metadata`
flag or environment variable on `bt trace run` overrides the persisted route
for that invocation only, without mutating the file.

### Root-span tags

Use repeatable `--tag` options to add filterable Braintrust tags to every root
span produced by a route. The option is available on persistent setup, one-off
runs, and transcript imports.

```bash
bt trace enable claude --tag ci --tag release-validation
bt trace run codex --tag ci -- "summarize this change"
bt trace import claude session-id --tag historical-import
Comment thread
Qard marked this conversation as resolved.
```

For automation, set `BRAINTRUST_TAGS` to a comma-separated list before running
one of those commands:

```bash
BRAINTRUST_TAGS=ci,release-validation bt trace run codex -- "summarize this change"
```

Use `bt trace disable <agent>` to remove the installed tracing plugin and its
Braintrust settings. `bt trace setup <agent>` remains an alias for `bt trace
enable <agent>` for backwards compatibility.
Expand Down
1 change: 1 addition & 0 deletions bt-daemon/src/delivery_ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ mod tests {
}),
flush_mode: FlushMode::FireAndForget,
additional_metadata: None,
tags: Vec::new(),
}
}

Expand Down
43 changes: 43 additions & 0 deletions bt-daemon/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ pub struct ImportArgs {
/// JSON object merged into every imported root span's metadata.
#[arg(long, env = "BRAINTRUST_ADDITIONAL_METADATA")]
pub additional_metadata: Option<String>,
/// Tag applied to each imported root span. May be repeated or comma-separated.
#[arg(long = "tag", env = "BRAINTRUST_TAGS", value_delimiter = ',')]
pub tags: Vec<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
Expand Down Expand Up @@ -266,6 +269,9 @@ pub struct RunArgs {
/// JSON object merged into root-span metadata for this invocation.
#[arg(long, env = "BRAINTRUST_ADDITIONAL_METADATA")]
pub additional_metadata: Option<String>,
/// Tag applied to each root span for this invocation. May be repeated or comma-separated.
#[arg(long = "tag", env = "BRAINTRUST_TAGS", value_delimiter = ',')]
pub tags: Vec<String>,
/// Arguments forwarded verbatim to the coding agent.
#[arg(allow_hyphen_values = true)]
pub agent_args: Vec<OsString>,
Expand Down Expand Up @@ -407,6 +413,26 @@ pub(crate) fn apply_additional_metadata(
Ok(())
}

/// Apply invocation-local root-span tags to a route. Tags are normalized once
/// at the CLI boundary so hook shims and translators only receive valid values.
pub(crate) fn apply_tags(route: &mut SessionRoute, tags: &[String]) -> anyhow::Result<()> {
if tags.is_empty() {
return Ok(());
}
let mut normalized = Vec::with_capacity(tags.len());
for tag in tags {
let tag = tag.trim();
if tag.is_empty() {
anyhow::bail!("--tag must not be empty");
}
if !normalized.iter().any(|existing| existing == tag) {
normalized.push(tag.to_string());
}
}
route.tags = normalized;
Ok(())
}

fn initialize_params(env: &Envelope) -> serde_json::Value {
serde_json::json!({
"protocol_version": PROTOCOL_VERSION,
Expand Down Expand Up @@ -1443,6 +1469,19 @@ mod tests {
.contains("invalid --additional-metadata JSON"));
}

#[test]
fn tags_override_a_route_and_are_normalized() {
let mut route = SessionRoute {
tags: vec!["saved".into()],
..SessionRoute::default()
};
apply_tags(&mut route, &[" ci ".into(), "ci".into(), "docs".into()]).unwrap();
assert_eq!(route.tags, ["ci", "docs"]);

let error = apply_tags(&mut route, &[" ".into()]).unwrap_err();
assert!(error.to_string().contains("must not be empty"));
}

#[test]
fn import_args_accept_multiple_sessions_or_all() {
let explicit = ImportCli::try_parse_from([
Expand Down Expand Up @@ -1544,6 +1583,7 @@ mod tests {
parent_project: None,
attach: true,
additional_metadata: None,
tags: Vec::new(),
};
assert!(validate_import_selection(&args)
.unwrap_err()
Expand Down Expand Up @@ -1624,6 +1664,7 @@ mod tests {
}),
flush_mode: wire::FlushMode::FireAndForget,
additional_metadata: None,
tags: Vec::new(),
}
}

Expand Down Expand Up @@ -1715,6 +1756,7 @@ mod tests {
RunArgs {
source: RunSource::Codex,
additional_metadata: None,
tags: Vec::new(),
agent_args: Vec::new(),
},
test_run_hook_command(),
Expand All @@ -1732,6 +1774,7 @@ mod tests {
RunArgs {
source: RunSource::Codex,
additional_metadata: None,
tags: Vec::new(),
agent_args: vec![OsString::from("--dangerously-bypass-hook-trust")],
},
test_run_hook_command(),
Expand Down
27 changes: 26 additions & 1 deletion bt-daemon/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,20 @@ fn enable_tracing_at(path: &Path, mut route: SessionRoute) -> anyhow::Result<()>
.filter(|metadata| metadata.is_object())
.cloned();
}
if route.tags.is_empty() {
route.tags = settings
.get("route")
.and_then(|route| route.get("tags"))
.or_else(|| settings.get("tags"))
.and_then(Value::as_array)
.map(|tags| {
tags.iter()
.filter_map(Value::as_str)
.map(ToOwned::to_owned)
.collect()
})
.unwrap_or_default();
}
settings.insert("trace_to_braintrust".into(), Value::Bool(true));
settings.insert("route".into(), serde_json::to_value(route)?);
for key in [
Expand All @@ -573,6 +587,7 @@ fn enable_tracing_at(path: &Path, mut route: SessionRoute) -> anyhow::Result<()>
"project",
"destination",
"additional_metadata",
"tags",
] {
settings.remove(key);
}
Expand Down Expand Up @@ -1203,7 +1218,11 @@ mod tests {
fn tracing_settings_preserve_metadata_until_setup_explicitly_replaces_it() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("braintrust.json");
std::fs::write(&path, r#"{"route":{"additional_metadata":{"ci":true}}}"#).unwrap();
std::fs::write(
&path,
r#"{"route":{"additional_metadata":{"ci":true},"tags":["saved"]}}"#,
)
.unwrap();

let route = SessionRoute::default();
enable_tracing_at(&path, route).unwrap();
Expand All @@ -1212,9 +1231,11 @@ mod tests {
settings["route"]["additional_metadata"],
serde_json::json!({"ci": true})
);
assert_eq!(settings["route"]["tags"], serde_json::json!(["saved"]));

let route = SessionRoute {
additional_metadata: Some(serde_json::json!({"run_id": "new"})),
tags: vec!["replacement".to_string()],
..SessionRoute::default()
};
enable_tracing_at(&path, route).unwrap();
Expand All @@ -1223,6 +1244,10 @@ mod tests {
settings["route"]["additional_metadata"],
serde_json::json!({"run_id": "new"})
);
assert_eq!(
settings["route"]["tags"],
serde_json::json!(["replacement"])
);
}

#[test]
Expand Down
40 changes: 40 additions & 0 deletions bt-daemon/src/trace_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ pub struct EnableArgs {
/// JSON object persisted in this agent's tracing route and merged into root-span metadata.
#[arg(long, global = true, env = "BRAINTRUST_ADDITIONAL_METADATA")]
pub additional_metadata: Option<String>,
/// Tag applied to each root span. May be repeated or comma-separated.
#[arg(
long = "tag",
global = true,
env = "BRAINTRUST_TAGS",
value_delimiter = ','
)]
pub tags: Vec<String>,
}

/// Backwards-compatible API name for hosts that mounted the former setup command.
Expand Down Expand Up @@ -155,6 +163,7 @@ mod tests {
TraceCommand::Setup(SetupArgs {
agent: SetupAgent::Claude,
additional_metadata: Some(ref value),
..
}) if value == r#"{"setup":true}"#
));

Expand Down Expand Up @@ -218,6 +227,37 @@ mod tests {
));
}

#[test]
fn setup_run_and_import_accept_tags() {
let setup =
Cli::try_parse_from(["bt", "setup", "claude", "--tag", "ci", "--tag", "docs"]).unwrap();
assert!(matches!(
setup.trace.command,
TraceCommand::Setup(SetupArgs { ref tags, .. }) if tags.as_slice() == ["ci", "docs"]
));

let run = Cli::try_parse_from(["bt", "run", "codex", "--tag", "ci,docs", "--", "status"])
.unwrap();
assert!(matches!(
run.trace.command,
TraceCommand::Run(RunArgs { ref tags, .. }) if tags.as_slice() == ["ci", "docs"]
));

let import = Cli::try_parse_from([
"bt",
"import",
"claude",
"session-id",
"--tag",
"historical",
])
.unwrap();
assert!(matches!(
import.trace.command,
TraceCommand::Import(ImportArgs { ref tags, .. }) if tags.as_slice() == ["historical"]
));
}

#[test]
fn doctor_accepts_every_supported_agent_alias() {
for (agent, expected, source, display_name) in [
Expand Down
17 changes: 12 additions & 5 deletions bt-daemon/src/trace_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
use crate::trace_command::{DoctorAgent, DoctorArgs, TraceCommand};
use crate::wire::{AuthSelection, AuthSource, SessionConfig, SessionRoute};
use crate::{
apply_additional_metadata, braintrust_serve_options, paths, run_disable, run_enable, run_hook,
run_import, run_serve, run_status, run_traced, shutdown_daemon, AuthDiagnostic, AuthLease,
AuthProvider, AuthResolveReason, BraintrustSinkConfig, DoctorCommandOutput, HostInfo,
OutputFormat, Registry, RunHookCommand, ServeOptions, StatusArgs, TraceArgs,
TraceCommandOutput,
apply_additional_metadata, apply_tags, braintrust_serve_options, paths, run_disable,
run_enable, run_hook, run_import, run_serve, run_status, run_traced, shutdown_daemon,
AuthDiagnostic, AuthLease, AuthProvider, AuthResolveReason, BraintrustSinkConfig,
DoctorCommandOutput, HostInfo, OutputFormat, Registry, RunHookCommand, ServeOptions,
StatusArgs, TraceArgs, TraceCommandOutput,
};
use async_trait::async_trait;
use std::ffi::OsString;
Expand Down Expand Up @@ -178,6 +178,7 @@ async fn session_config(
destination: route.destination.clone(),
flush_mode: route.flush_mode,
additional_metadata: route.additional_metadata.clone(),
tags: route.tags.clone(),
})
}

Expand Down Expand Up @@ -328,6 +329,7 @@ pub async fn run_trace(args: TraceArgs, host: TraceHostContext) -> anyhow::Resul
)
.await?;
apply_additional_metadata(&mut route, enable_args.additional_metadata.as_deref())?;
apply_tags(&mut route, &enable_args.tags)?;
print_output(run_enable(enable_args, route)?, host.output_format)
}
TraceCommand::Disable(disable_args) => {
Expand Down Expand Up @@ -382,6 +384,7 @@ pub async fn run_trace(args: TraceArgs, host: TraceHostContext) -> anyhow::Resul
})
.await?;
apply_additional_metadata(&mut route, import_args.additional_metadata.as_deref())?;
apply_tags(&mut route, &import_args.tags)?;
let config = session_config(&host, &route).await?;
let summaries = run_import(import_args, serve_options(&host), Some(config)).await?;
print_output(TraceCommandOutput::import(summaries), host.output_format)
Expand All @@ -397,6 +400,7 @@ pub async fn run_trace(args: TraceArgs, host: TraceHostContext) -> anyhow::Resul
)
.await?;
apply_additional_metadata(&mut route, run_args.additional_metadata.as_deref())?;
apply_tags(&mut route, &run_args.tags)?;
let hook_command = child_command(&host.command, "hook");
let status = run_traced(run_args, hook_command, route).await?;
if status.success() {
Expand Down Expand Up @@ -572,13 +576,15 @@ mod tests {
TraceCommand::Setup(SetupArgs {
agent: SetupAgent::OpenCode,
additional_metadata: None,
tags: Vec::new(),
}),
true,
),
(
TraceCommand::Run(RunArgs {
source: RunSource::Codex,
additional_metadata: None,
tags: Vec::new(),
agent_args: Vec::new(),
}),
false,
Expand Down Expand Up @@ -673,6 +679,7 @@ mod tests {
parent_project: None,
attach: false,
additional_metadata: None,
tags: Vec::new(),
};
let error = run_trace(
TraceArgs {
Expand Down
4 changes: 3 additions & 1 deletion bt-daemon/src/translate/antigravity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
use super::git::GitMetadataCache;
use super::tool::{with_tool_approval, ToolApproval};
use super::{
local_username, AgentTranslator, SessionCtx, SpanOp, SpanRow, SpanType, TranslatorFactory,
local_username, root_tags, AgentTranslator, SessionCtx, SpanOp, SpanRow, SpanType,
TranslatorFactory,
};
use crate::ids;
use crate::wire::Envelope;
Expand Down Expand Up @@ -155,6 +156,7 @@ impl AntigravityTranslator {
span_type: SpanType::Task,
start_ms: Some(event.ts_ms),
metadata: Some(Value::Object(metadata)),
tags: root_tags(ctx),
..Default::default()
}));
}
Expand Down
4 changes: 3 additions & 1 deletion bt-daemon/src/translate/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use super::git::GitMetadataCache;
use super::recent::RecentSet;
use super::tool::{add_tool_approval, nonempty_error_text, ToolApproval};
use super::{
local_username, AgentTranslator, SessionCtx, SpanOp, SpanRow, SpanType, TranslatorFactory,
local_username, root_tags, AgentTranslator, SessionCtx, SpanOp, SpanRow, SpanType,
TranslatorFactory,
};
use crate::ids;
use crate::wire::Envelope;
Expand Down Expand Up @@ -259,6 +260,7 @@ impl ClaudeTranslator {
start_ms: Some(event.ts_ms),
input: Some(json!(format!("Session: {workspace}"))),
metadata: Some(Value::Object(metadata)),
tags: root_tags(ctx),
..Default::default()
}));
}
Expand Down
Loading
Loading