mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Add TUI tools for managing Codex tasks (#40308)
## Why The TUI rejected app-server dynamic tool calls as unsupported, so agents could not inspect or manage other Codex tasks through a TUI session. ## What changed - Add the `codex_tui` tool namespace for listing, reading, waiting on, creating, forking, messaging, renaming, archiving, and restoring tasks. - Handle tool calls through existing app-server thread operations, with bounded inputs and responses and cleanup when calls complete or the TUI disconnects. - Route delegation tools through an authenticated local MCP server with explicit approval prompts, while respecting configured and managed MCP policies. - Fall back to starting threads without dynamic tools when connected to an older app server that does not support them. ## Testing Add coverage for tool schemas and response bounds, task lifecycle operations, approval-gated MCP transport, namespace dispatch, cancellation, and legacy app-server fallback behavior. GitOrigin-RevId: c782c11f0a3edceb3fa6909a4b92d6ce01a6eed3
This commit is contained in:
1 parent
fb0781b9ee
commit
a8468330bb
18 files changed
+3722
-62
No files matched your search
@@ -12,7 +12,10 @@ pub(crate) use history::HISTORY_ITEM_SCAN_LIMIT;
|
||||
pub(crate) use history::HistoryHydrationScope;
|
||||
pub(crate) use history::thread_items_page_params;
|
||||
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::FeedbackAudience;
|
||||
use crate::dynamic_tools_mcp::DynamicToolMcpServer;
|
||||
use crate::dynamic_tools_mcp::ThreadToolTransport;
|
||||
use crate::legacy_core::config::Config;
|
||||
use crate::service_tier_resolution;
|
||||
use crate::session_state::MessageHistoryMetadata;
|
||||
@@ -103,6 +106,7 @@ use codex_app_server_protocol::ThreadSource;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStartSource;
|
||||
use codex_app_server_protocol::ThreadStatusChangedNotification;
|
||||
use codex_app_server_protocol::ThreadUnarchiveParams;
|
||||
use codex_app_server_protocol::ThreadUnarchiveResponse;
|
||||
use codex_app_server_protocol::ThreadUnsubscribeParams;
|
||||
@@ -137,6 +141,7 @@ use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
@@ -162,7 +167,7 @@ enum ForkPresentation {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ThreadHistorySupport {
|
||||
pub(crate) enum ThreadHistorySupport {
|
||||
Paginated,
|
||||
LegacyOnly,
|
||||
}
|
||||
@@ -200,35 +205,50 @@ pub(crate) fn is_history_pagination_unsupported(source: &JSONRPCErrorError) -> b
|
||||
.any(|error| message.contains(error)))
|
||||
}
|
||||
|
||||
async fn request_thread_start_with_history_fallback(
|
||||
pub(crate) async fn request_thread_start_with_history_fallback(
|
||||
request_handle: &AppServerRequestHandle,
|
||||
request_id: RequestId,
|
||||
mut request_id: RequestId,
|
||||
mut params: ThreadStartParams,
|
||||
) -> std::result::Result<(ThreadStartResponse, ThreadHistorySupport), TypedRequestError> {
|
||||
match request_handle
|
||||
.request_typed(ClientRequest::ThreadStart {
|
||||
request_id,
|
||||
params: params.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(response) => Ok((response, ThreadHistorySupport::Paginated)),
|
||||
Err(TypedRequestError::Server { source, .. })
|
||||
if params.history_mode.is_some() && is_history_pagination_unsupported(&source) =>
|
||||
let mut history_support = ThreadHistorySupport::Paginated;
|
||||
loop {
|
||||
match request_handle
|
||||
.request_typed(ClientRequest::ThreadStart {
|
||||
request_id,
|
||||
params: params.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
params.history_mode = None;
|
||||
let response = request_handle
|
||||
.request_typed(ClientRequest::ThreadStart {
|
||||
request_id: RequestId::String(format!(
|
||||
"legacy-thread-start-{}",
|
||||
Uuid::new_v4()
|
||||
)),
|
||||
params,
|
||||
})
|
||||
.await?;
|
||||
Ok((response, ThreadHistorySupport::LegacyOnly))
|
||||
Ok(response) => return Ok((response, history_support)),
|
||||
Err(TypedRequestError::Server { source, .. })
|
||||
if params.history_mode.is_some() && is_history_pagination_unsupported(&source) =>
|
||||
{
|
||||
params.history_mode = None;
|
||||
history_support = ThreadHistorySupport::LegacyOnly;
|
||||
request_id = RequestId::String(format!("legacy-thread-start-{}", Uuid::new_v4()));
|
||||
}
|
||||
Err(TypedRequestError::Server { source, .. })
|
||||
if params.dynamic_tools.is_some()
|
||||
&& matches!(
|
||||
source.code,
|
||||
JSONRPC_INVALID_REQUEST | JSONRPC_INVALID_PARAMS
|
||||
)
|
||||
&& {
|
||||
let message = source.message.to_ascii_lowercase();
|
||||
["dynamictools", "dynamic tool", "namespace", "inputschema"]
|
||||
.into_iter()
|
||||
.any(|field| message.contains(field))
|
||||
} =>
|
||||
{
|
||||
tracing::warn!(
|
||||
error = %source.message,
|
||||
"app server does not support TUI dynamic tools; starting without them"
|
||||
);
|
||||
params.dynamic_tools = None;
|
||||
request_id = RequestId::String(format!("legacy-thread-start-{}", Uuid::new_v4()));
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +293,7 @@ pub(crate) struct AppServerSession {
|
||||
available_models: Vec<ModelPreset>,
|
||||
managed_new_thread_defaults: Option<NewThreadModelDefaults>,
|
||||
external_agent_config_import_completion_pending: AtomicBool,
|
||||
dynamic_tool_mcp: Option<Arc<DynamicToolMcpServer>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -356,6 +377,72 @@ impl AppServerSession {
|
||||
available_models: Vec::new(),
|
||||
managed_new_thread_defaults: None,
|
||||
external_agent_config_import_completion_pending: AtomicBool::new(false),
|
||||
dynamic_tool_mcp: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn start_dynamic_tool_mcp(
|
||||
&mut self,
|
||||
config: Config,
|
||||
app_event_tx: AppEventSender,
|
||||
status_updates: tokio::sync::broadcast::Sender<ThreadStatusChangedNotification>,
|
||||
) -> std::io::Result<()> {
|
||||
if self.uses_embedded_app_server() {
|
||||
return Ok(());
|
||||
}
|
||||
if config
|
||||
.mcp_servers
|
||||
.get()
|
||||
.contains_key(crate::dynamic_tools::NAMESPACE)
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::AlreadyExists,
|
||||
"a user-configured MCP server already owns the codex_tui namespace",
|
||||
));
|
||||
}
|
||||
let managed_requirement = config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.mcp_servers
|
||||
.as_ref()
|
||||
.map(|requirements| {
|
||||
requirements
|
||||
.value
|
||||
.get(crate::dynamic_tools::NAMESPACE)
|
||||
.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"managed MCP requirements do not permit the TUI task-tools server",
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
let thread_start_params = thread_start_params_from_config(
|
||||
&config,
|
||||
self.thread_params_mode(),
|
||||
self.remote_cwd_override(),
|
||||
/*session_start_source*/ None,
|
||||
);
|
||||
self.dynamic_tool_mcp = Some(Arc::new(
|
||||
DynamicToolMcpServer::start(
|
||||
self.request_handle(),
|
||||
thread_start_params,
|
||||
app_event_tx,
|
||||
status_updates,
|
||||
managed_requirement,
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn thread_tool_transport(&self) -> ThreadToolTransport {
|
||||
if self.uses_embedded_app_server() {
|
||||
ThreadToolTransport::Disabled
|
||||
} else if let Some(server) = self.dynamic_tool_mcp.as_ref() {
|
||||
ThreadToolTransport::Mcp(Arc::clone(server))
|
||||
} else {
|
||||
ThreadToolTransport::Dynamic
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,6 +718,7 @@ impl AppServerSession {
|
||||
if self.history_support == ThreadHistorySupport::LegacyOnly {
|
||||
params.history_mode = None;
|
||||
}
|
||||
self.thread_tool_transport().configure(&mut params);
|
||||
let request_handle = self.request_handle();
|
||||
let (response, history_support) =
|
||||
request_thread_start_with_history_fallback(&request_handle, request_id, params)
|
||||
@@ -729,6 +817,8 @@ impl AppServerSession {
|
||||
self.remote_cwd_override.as_deref(),
|
||||
)
|
||||
};
|
||||
self.thread_tool_transport()
|
||||
.configure_mcp(&mut params.config);
|
||||
let response: ThreadForkResponse = match self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadFork {
|
||||
@@ -1429,14 +1519,16 @@ pub(crate) async fn start_thread_with_request_handle(
|
||||
config: Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd_override: Option<PathBuf>,
|
||||
thread_tool_transport: ThreadToolTransport,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let request_id = RequestId::String(format!("startup-thread-start-{}", Uuid::new_v4()));
|
||||
let params = thread_start_params_from_config(
|
||||
let mut params = thread_start_params_from_config(
|
||||
&config,
|
||||
thread_params_mode,
|
||||
remote_cwd_override.as_deref(),
|
||||
/*session_start_source*/ None,
|
||||
);
|
||||
thread_tool_transport.configure(&mut params);
|
||||
let (response, _history_support) =
|
||||
request_thread_start_with_history_fallback(&request_handle, request_id, params)
|
||||
.await
|
||||
@@ -1686,7 +1778,7 @@ fn permissions_selection_from_config(
|
||||
.map(permission_profile_id_from_active_profile)
|
||||
}
|
||||
|
||||
fn thread_start_params_from_config(
|
||||
pub(crate) fn thread_start_params_from_config(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
@@ -2516,6 +2608,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(params.model_provider, Some(config.model_provider_id));
|
||||
assert_eq!(params.thread_source, Some(ThreadSource::User));
|
||||
assert_eq!(params.dynamic_tools, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in new issue
Block a user