Populate workspace kind on Codex turn events (#25135)

## Summary
- carry `workspace_kind` from Responses API client metadata into the
turn resolved analytics fact
- serialize the optional value on `codex_turn_event`
- cover both the turn metadata source and turn event serialization

The `workspace_kind` tells us whether a thread had a project attached vs
projectless. this is an indicator for who is adopting Codex for
knowledge work outside of coding

## Testing
- `env UV_CACHE_DIR=/private/tmp/uv-cache
/private/tmp/cargo-tools/bin/just fmt`
- `env PATH=/private/tmp/cargo-tools/bin:$PATH
CARGO_HOME=/private/tmp/cargo-home UV_CACHE_DIR=/private/tmp/uv-cache
/private/tmp/cargo-tools/bin/just test -p codex-analytics`
- `env PATH=/private/tmp/cargo-tools/bin:$PATH
CARGO_HOME=/private/tmp/cargo-home UV_CACHE_DIR=/private/tmp/uv-cache
/private/tmp/cargo-tools/bin/just test -p codex-core turn_metadata`

Paired with openai/openai#970661, which keeps forwarding the same
metadata key through Responses API headers.
This commit is contained in:
knittel-openai
2026-06-02 12:46:14 -07:00
committed by GitHub
parent ad355d4c96
commit b794182ea7
8 changed files with 26 additions and 0 deletions
@@ -391,6 +391,7 @@ fn sample_turn_resolved_config(thread_id: &str, turn_id: &str) -> TurnResolvedCo
sandbox_network_access: true,
collaboration_mode: ModeKind::Plan,
personality: None,
workspace_kind: None,
is_first_turn: true,
}
}
@@ -3255,6 +3256,7 @@ fn turn_event_serializes_expected_shape() {
sandbox_network_access: true,
collaboration_mode: Some("plan"),
personality: Some("pragmatic".to_string()),
workspace_kind: Some("projectless".to_string()),
num_input_images: 2,
is_first_turn: true,
status: Some(TurnStatus::Completed),
@@ -3320,6 +3322,7 @@ fn turn_event_serializes_expected_shape() {
"sandbox_network_access": true,
"collaboration_mode": "plan",
"personality": "pragmatic",
"workspace_kind": "projectless",
"num_input_images": 2,
"is_first_turn": true,
"status": "completed",
@@ -3640,6 +3643,7 @@ async fn turn_lifecycle_emits_turn_event() {
);
assert!(payload["event_params"].get("product_client_id").is_none());
assert_eq!(payload["event_params"]["ephemeral"], json!(false));
assert_eq!(payload["event_params"]["workspace_kind"], json!(null));
assert_eq!(payload["event_params"]["num_input_images"], json!(1));
assert_eq!(payload["event_params"]["status"], json!("completed"));
assert_eq!(payload["event_params"]["steer_count"], json!(0));
+1
View File
@@ -794,6 +794,7 @@ pub(crate) struct CodexTurnEventParams {
pub(crate) sandbox_network_access: bool,
pub(crate) collaboration_mode: Option<&'static str>,
pub(crate) personality: Option<String>,
pub(crate) workspace_kind: Option<String>,
pub(crate) num_input_images: usize,
pub(crate) is_first_turn: bool,
pub(crate) status: Option<TurnStatus>,
+1
View File
@@ -85,6 +85,7 @@ pub struct TurnResolvedConfigFact {
pub sandbox_network_access: bool,
pub collaboration_mode: ModeKind,
pub personality: Option<Personality>,
pub workspace_kind: Option<String>,
pub is_first_turn: bool,
}
+2
View File
@@ -2483,6 +2483,7 @@ fn codex_turn_event_params(
sandbox_network_access,
collaboration_mode,
personality,
workspace_kind,
is_first_turn,
} = resolved_config;
let token_usage = turn_state.token_usage.clone();
@@ -2515,6 +2516,7 @@ fn codex_turn_event_params(
sandbox_network_access,
collaboration_mode: Some(collaboration_mode_mode(collaboration_mode)),
personality: personality_mode(personality),
workspace_kind,
num_input_images,
is_first_turn,
status: completed.status,