mirror of
https://github.com/openai/codex.git
synced 2026-11-15 21:50:46 +00:00
## Summary Some background. We're looking to instrument GA turns end to end. Right now a big gap is grouping mcp tool calls with their codex sessions. We send session id and turn id headers to the responses call but not the mcp/wham calls. Ideally we could pass the args as headers like with responses, but given the setup of the rmcp client, we can't send as headers without either changing the rmcp package upstream to allow per request headers or introducing a mutex which break concurrency. An earlier attempt made the assumption that we had 1 client per thread, which allowed us to set headers at the start of a turn. @pakrym mentioned that this assumption might break in the near future. So the solution now is to package the turn metadata/session id into the _meta field in the post body and pull out in codex-backend. - send turn metadata to MCP servers via `tools/call` `_meta` instead of assuming per-thread request headers on shared clients - preserve the existing `_codex_apps` metadata while adding `x-codex-turn-metadata` for all MCP tool calls - extend tests to cover both custom MCP servers and the codex apps search flow --------- Co-authored-by: Codex <noreply@openai.com>
251 lines
7.5 KiB
Rust
251 lines
7.5 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
use std::sync::RwLock;
|
|
|
|
use serde::Serialize;
|
|
use tokio::task::JoinHandle;
|
|
|
|
use crate::git_info::get_git_remote_urls_assume_git_repo;
|
|
use crate::git_info::get_git_repo_root;
|
|
use crate::git_info::get_has_changes;
|
|
use crate::git_info::get_head_commit_hash;
|
|
use crate::sandbox_tags::sandbox_tag;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
struct WorkspaceGitMetadata {
|
|
associated_remote_urls: Option<BTreeMap<String, String>>,
|
|
latest_git_commit_hash: Option<String>,
|
|
has_changes: Option<bool>,
|
|
}
|
|
|
|
impl WorkspaceGitMetadata {
|
|
fn is_empty(&self) -> bool {
|
|
self.associated_remote_urls.is_none()
|
|
&& self.latest_git_commit_hash.is_none()
|
|
&& self.has_changes.is_none()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Default)]
|
|
struct TurnMetadataWorkspace {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
associated_remote_urls: Option<BTreeMap<String, String>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
latest_git_commit_hash: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
has_changes: Option<bool>,
|
|
}
|
|
|
|
impl From<WorkspaceGitMetadata> for TurnMetadataWorkspace {
|
|
fn from(value: WorkspaceGitMetadata) -> Self {
|
|
Self {
|
|
associated_remote_urls: value.associated_remote_urls,
|
|
latest_git_commit_hash: value.latest_git_commit_hash,
|
|
has_changes: value.has_changes,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Default)]
|
|
pub(crate) struct TurnMetadataBag {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
session_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
turn_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
|
workspaces: BTreeMap<String, TurnMetadataWorkspace>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
sandbox: Option<String>,
|
|
}
|
|
|
|
impl TurnMetadataBag {
|
|
fn to_header_value(&self) -> Option<String> {
|
|
serde_json::to_string(self).ok()
|
|
}
|
|
}
|
|
|
|
fn build_turn_metadata_bag(
|
|
session_id: Option<String>,
|
|
turn_id: Option<String>,
|
|
sandbox: Option<String>,
|
|
repo_root: Option<String>,
|
|
workspace_git_metadata: Option<WorkspaceGitMetadata>,
|
|
) -> TurnMetadataBag {
|
|
let mut workspaces = BTreeMap::new();
|
|
if let (Some(repo_root), Some(workspace_git_metadata)) = (repo_root, workspace_git_metadata)
|
|
&& !workspace_git_metadata.is_empty()
|
|
{
|
|
workspaces.insert(repo_root, workspace_git_metadata.into());
|
|
}
|
|
|
|
TurnMetadataBag {
|
|
session_id,
|
|
turn_id,
|
|
workspaces,
|
|
sandbox,
|
|
}
|
|
}
|
|
|
|
pub async fn build_turn_metadata_header(cwd: &Path, sandbox: Option<&str>) -> Option<String> {
|
|
let repo_root = get_git_repo_root(cwd).map(|root| root.to_string_lossy().into_owned());
|
|
|
|
let (latest_git_commit_hash, associated_remote_urls, has_changes) = tokio::join!(
|
|
get_head_commit_hash(cwd),
|
|
get_git_remote_urls_assume_git_repo(cwd),
|
|
get_has_changes(cwd),
|
|
);
|
|
if latest_git_commit_hash.is_none()
|
|
&& associated_remote_urls.is_none()
|
|
&& has_changes.is_none()
|
|
&& sandbox.is_none()
|
|
{
|
|
return None;
|
|
}
|
|
|
|
build_turn_metadata_bag(
|
|
/*session_id*/ None,
|
|
/*turn_id*/ None,
|
|
sandbox.map(ToString::to_string),
|
|
repo_root,
|
|
Some(WorkspaceGitMetadata {
|
|
associated_remote_urls,
|
|
latest_git_commit_hash,
|
|
has_changes,
|
|
}),
|
|
)
|
|
.to_header_value()
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct TurnMetadataState {
|
|
cwd: PathBuf,
|
|
repo_root: Option<String>,
|
|
base_metadata: TurnMetadataBag,
|
|
base_header: String,
|
|
enriched_header: Arc<RwLock<Option<String>>>,
|
|
enrichment_task: Arc<Mutex<Option<JoinHandle<()>>>>,
|
|
}
|
|
|
|
impl TurnMetadataState {
|
|
pub(crate) fn new(
|
|
session_id: String,
|
|
turn_id: String,
|
|
cwd: PathBuf,
|
|
sandbox_policy: &SandboxPolicy,
|
|
windows_sandbox_level: WindowsSandboxLevel,
|
|
) -> Self {
|
|
let repo_root = get_git_repo_root(&cwd).map(|root| root.to_string_lossy().into_owned());
|
|
let sandbox = Some(sandbox_tag(sandbox_policy, windows_sandbox_level).to_string());
|
|
let base_metadata = build_turn_metadata_bag(
|
|
Some(session_id),
|
|
Some(turn_id),
|
|
sandbox,
|
|
/*repo_root*/ None,
|
|
/*workspace_git_metadata*/ None,
|
|
);
|
|
let base_header = base_metadata
|
|
.to_header_value()
|
|
.unwrap_or_else(|| "{}".to_string());
|
|
|
|
Self {
|
|
cwd,
|
|
repo_root,
|
|
base_metadata,
|
|
base_header,
|
|
enriched_header: Arc::new(RwLock::new(None)),
|
|
enrichment_task: Arc::new(Mutex::new(None)),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn current_header_value(&self) -> Option<String> {
|
|
if let Some(header) = self
|
|
.enriched_header
|
|
.read()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.as_ref()
|
|
.cloned()
|
|
{
|
|
return Some(header);
|
|
}
|
|
Some(self.base_header.clone())
|
|
}
|
|
|
|
pub(crate) fn current_meta_value(&self) -> Option<serde_json::Value> {
|
|
self.current_header_value()
|
|
.and_then(|header| serde_json::from_str(&header).ok())
|
|
}
|
|
|
|
pub(crate) fn spawn_git_enrichment_task(&self) {
|
|
if self.repo_root.is_none() {
|
|
return;
|
|
}
|
|
|
|
let mut task_guard = self
|
|
.enrichment_task
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
if task_guard.is_some() {
|
|
return;
|
|
}
|
|
|
|
let state = self.clone();
|
|
*task_guard = Some(tokio::spawn(async move {
|
|
let workspace_git_metadata = state.fetch_workspace_git_metadata().await;
|
|
let Some(repo_root) = state.repo_root.clone() else {
|
|
return;
|
|
};
|
|
|
|
let enriched_metadata = build_turn_metadata_bag(
|
|
state.base_metadata.session_id.clone(),
|
|
state.base_metadata.turn_id.clone(),
|
|
state.base_metadata.sandbox.clone(),
|
|
Some(repo_root),
|
|
Some(workspace_git_metadata),
|
|
);
|
|
if enriched_metadata.workspaces.is_empty() {
|
|
return;
|
|
}
|
|
|
|
if let Some(header_value) = enriched_metadata.to_header_value() {
|
|
*state
|
|
.enriched_header
|
|
.write()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(header_value);
|
|
}
|
|
}));
|
|
}
|
|
|
|
pub(crate) fn cancel_git_enrichment_task(&self) {
|
|
let mut task_guard = self
|
|
.enrichment_task
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
if let Some(task) = task_guard.take() {
|
|
task.abort();
|
|
}
|
|
}
|
|
|
|
async fn fetch_workspace_git_metadata(&self) -> WorkspaceGitMetadata {
|
|
let (latest_git_commit_hash, associated_remote_urls, has_changes) = tokio::join!(
|
|
get_head_commit_hash(&self.cwd),
|
|
get_git_remote_urls_assume_git_repo(&self.cwd),
|
|
get_has_changes(&self.cwd),
|
|
);
|
|
|
|
WorkspaceGitMetadata {
|
|
associated_remote_urls,
|
|
latest_git_commit_hash,
|
|
has_changes,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "turn_metadata_tests.rs"]
|
|
mod tests;
|