mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(core/tools): wire dynamic tool pipeline
This commit is contained in:
@@ -19,6 +19,8 @@ pub use code_protocol::mcp_protocol::{
|
||||
ConversationId,
|
||||
ExecCommandApprovalParams,
|
||||
ExecCommandApprovalResponse,
|
||||
DynamicToolCallParams,
|
||||
DynamicToolCallResponse,
|
||||
GetAuthStatusParams,
|
||||
GetAuthStatusResponse,
|
||||
GetUserAgentResponse,
|
||||
@@ -59,6 +61,7 @@ pub use code_protocol::mcp_protocol::{
|
||||
UserSavedConfig,
|
||||
APPLY_PATCH_APPROVAL_METHOD,
|
||||
EXEC_COMMAND_APPROVAL_METHOD,
|
||||
DYNAMIC_TOOL_CALL_METHOD,
|
||||
};
|
||||
pub use mcp_types::{
|
||||
JSONRPCError,
|
||||
|
||||
@@ -31,6 +31,7 @@ use crate::outgoing_message::OutgoingMessageSender;
|
||||
use crate::outgoing_message::OutgoingNotification;
|
||||
use crate::fuzzy_file_search::run_fuzzy_file_search;
|
||||
use code_protocol::protocol::TurnAbortReason;
|
||||
use code_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
|
||||
use code_core::protocol::InputItem as CoreInputItem;
|
||||
use code_core::protocol::Op;
|
||||
use code_core::protocol as core_protocol;
|
||||
@@ -41,7 +42,10 @@ use code_protocol::mcp_protocol::ApplyPatchApprovalParams;
|
||||
use code_protocol::mcp_protocol::ApplyPatchApprovalResponse;
|
||||
use code_protocol::mcp_protocol::ClientRequest;
|
||||
use code_protocol::mcp_protocol::ConversationId;
|
||||
use code_protocol::mcp_protocol::DynamicToolCallParams;
|
||||
use code_protocol::mcp_protocol::DynamicToolCallResponse;
|
||||
use code_protocol::mcp_protocol::EXEC_COMMAND_APPROVAL_METHOD;
|
||||
use code_protocol::mcp_protocol::DYNAMIC_TOOL_CALL_METHOD;
|
||||
use code_protocol::mcp_protocol::ExecCommandApprovalParams;
|
||||
use code_protocol::mcp_protocol::ExecCommandApprovalResponse;
|
||||
use code_protocol::mcp_protocol::InputItem as WireInputItem;
|
||||
@@ -623,6 +627,24 @@ async fn apply_bespoke_event_handling(
|
||||
on_exec_approval_response(approval_id, rx, conversation).await;
|
||||
});
|
||||
}
|
||||
EventMsg::DynamicToolCallRequest(request) => {
|
||||
let call_id = request.call_id;
|
||||
let params = DynamicToolCallParams {
|
||||
conversation_id,
|
||||
turn_id: request.turn_id,
|
||||
call_id: call_id.clone(),
|
||||
tool: request.tool,
|
||||
arguments: request.arguments,
|
||||
};
|
||||
let value = serde_json::to_value(¶ms).unwrap_or_default();
|
||||
let rx = outgoing
|
||||
.send_request(DYNAMIC_TOOL_CALL_METHOD, Some(value))
|
||||
.await;
|
||||
|
||||
tokio::spawn(async move {
|
||||
on_dynamic_tool_call_response(call_id, rx, conversation).await;
|
||||
});
|
||||
}
|
||||
// No special handling needed for interrupts; responses are sent immediately.
|
||||
|
||||
_ => {}
|
||||
@@ -642,6 +664,7 @@ fn derive_config_from_params(
|
||||
config: cli_overrides,
|
||||
base_instructions,
|
||||
include_plan_tool,
|
||||
dynamic_tools,
|
||||
..
|
||||
} = params;
|
||||
let overrides = ConfigOverrides {
|
||||
@@ -663,6 +686,7 @@ fn derive_config_from_params(
|
||||
tools_web_search_request: None,
|
||||
mcp_servers: None,
|
||||
experimental_client_tools: None,
|
||||
dynamic_tools,
|
||||
compact_prompt_override: None,
|
||||
compact_prompt_override_file: None,
|
||||
};
|
||||
@@ -718,6 +742,58 @@ async fn on_patch_approval_response(
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_dynamic_tool_call_response(
|
||||
call_id: String,
|
||||
receiver: tokio::sync::oneshot::Receiver<mcp_types::Result>,
|
||||
conversation: Arc<CodexConversation>,
|
||||
) {
|
||||
let response = receiver.await;
|
||||
let value = match response {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
error!("request failed: {err:?}");
|
||||
let fallback = CoreDynamicToolResponse {
|
||||
call_id: call_id.clone(),
|
||||
output: "dynamic tool request failed".to_string(),
|
||||
success: false,
|
||||
};
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::DynamicToolResponse {
|
||||
id: call_id.clone(),
|
||||
response: fallback,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit DynamicToolResponse: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let response = serde_json::from_value::<DynamicToolCallResponse>(value).unwrap_or_else(|err| {
|
||||
error!("failed to deserialize DynamicToolCallResponse: {err}");
|
||||
DynamicToolCallResponse {
|
||||
output: "dynamic tool response was invalid".to_string(),
|
||||
success: false,
|
||||
}
|
||||
});
|
||||
|
||||
let response = CoreDynamicToolResponse {
|
||||
call_id: call_id.clone(),
|
||||
output: response.output,
|
||||
success: response.success,
|
||||
};
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::DynamicToolResponse {
|
||||
id: call_id,
|
||||
response,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit DynamicToolResponse: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_exec_approval_response(
|
||||
approval_id: String,
|
||||
receiver: tokio::sync::oneshot::Receiver<mcp_types::Result>,
|
||||
|
||||
@@ -1123,6 +1123,7 @@ impl Codex {
|
||||
cwd: config.cwd.clone(),
|
||||
resume_path: resume_path.clone(),
|
||||
demo_developer_message: config.demo_developer_message.clone(),
|
||||
dynamic_tools: config.dynamic_tools.clone(),
|
||||
};
|
||||
|
||||
let config = Arc::new(config);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use serde_json::Value;
|
||||
use code_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use code_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use super::streaming::{
|
||||
AgentTask,
|
||||
TRUNCATION_MARKER,
|
||||
@@ -134,6 +135,7 @@ pub(super) struct State {
|
||||
pub(super) current_task: Option<AgentTask>,
|
||||
pub(super) pending_approvals: HashMap<String, oneshot::Sender<ReviewDecision>>,
|
||||
pub(super) pending_request_user_input: HashMap<String, oneshot::Sender<crate::protocol::RequestUserInputResponse>>,
|
||||
pub(super) pending_dynamic_tools: HashMap<String, oneshot::Sender<DynamicToolResponse>>,
|
||||
pub(super) pending_input: Vec<ResponseInputItem>,
|
||||
pub(super) pending_user_input: Vec<QueuedUserInput>,
|
||||
pub(super) history: ConversationHistory,
|
||||
@@ -328,6 +330,7 @@ pub(crate) struct Session {
|
||||
pub(super) _writable_roots: Vec<PathBuf>,
|
||||
pub(super) disable_response_storage: bool,
|
||||
pub(super) tools_config: ToolsConfig,
|
||||
pub(super) dynamic_tools: Vec<DynamicToolSpec>,
|
||||
|
||||
/// Manager for external MCP servers/tools.
|
||||
pub(super) mcp_connection_manager: McpConnectionManager,
|
||||
@@ -421,6 +424,10 @@ impl Session {
|
||||
self.approval_policy
|
||||
}
|
||||
|
||||
pub(crate) fn is_dynamic_tool(&self, name: &str) -> bool {
|
||||
self.dynamic_tools.iter().any(|tool| tool.name == name)
|
||||
}
|
||||
|
||||
fn next_background_sequence(&self, sub_id: &str) -> u64 {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let entry = state
|
||||
@@ -1150,8 +1157,29 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_dynamic_tool_response(&self, call_id: &str, _response: DynamicToolResponse) {
|
||||
tracing::warn!("dropping dynamic tool response for call_id={call_id}");
|
||||
pub fn register_pending_dynamic_tool(
|
||||
&self,
|
||||
call_id: String,
|
||||
) -> std::result::Result<oneshot::Receiver<DynamicToolResponse>, String> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if state.pending_dynamic_tools.contains_key(&call_id) {
|
||||
return Err(format!("dynamic tool already pending for call_id={call_id}"));
|
||||
}
|
||||
state.pending_dynamic_tools.insert(call_id, tx);
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
pub fn notify_dynamic_tool_response(&self, call_id: &str, response: DynamicToolResponse) {
|
||||
let pending = {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.pending_dynamic_tools.remove(call_id)
|
||||
};
|
||||
if let Some(tx) = pending {
|
||||
let _ = tx.send(response);
|
||||
} else {
|
||||
tracing::warn!("no pending dynamic tool found for call_id={call_id}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_approved_command(&self, pattern: ApprovedCommandPattern) {
|
||||
@@ -1874,6 +1902,7 @@ impl Session {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.pending_approvals.clear();
|
||||
state.pending_request_user_input.clear();
|
||||
state.pending_dynamic_tools.clear();
|
||||
// Do not clear `pending_input` here. When a user submits a new message
|
||||
// immediately after an interrupt, it may have been routed to
|
||||
// `pending_input` by an earlier code path. Clearing it would drop the
|
||||
|
||||
@@ -256,6 +256,7 @@ pub(super) async fn submission_loop(
|
||||
cwd,
|
||||
resume_path,
|
||||
demo_developer_message,
|
||||
dynamic_tools,
|
||||
} => {
|
||||
debug!(
|
||||
"Configuring session: model={model}; provider={provider:?}; resume={resume_path:?}"
|
||||
@@ -305,6 +306,7 @@ pub(super) async fn submission_loop(
|
||||
updated_config.disable_response_storage = disable_response_storage;
|
||||
updated_config.notify = notify.clone();
|
||||
updated_config.cwd = cwd.clone();
|
||||
updated_config.dynamic_tools = dynamic_tools.clone();
|
||||
|
||||
updated_config.model_family = find_family_for_model(&updated_config.model)
|
||||
.unwrap_or_else(|| derive_default_model_family(&updated_config.model));
|
||||
@@ -633,6 +635,7 @@ pub(super) async fn submission_loop(
|
||||
client,
|
||||
remote_models_manager,
|
||||
tools_config,
|
||||
dynamic_tools,
|
||||
tx_event: tx_event.clone(),
|
||||
user_instructions: effective_user_instructions.clone(),
|
||||
base_instructions,
|
||||
@@ -1965,6 +1968,7 @@ async fn run_turn(
|
||||
Some(sess.mcp_connection_manager.list_all_tools()),
|
||||
browser_enabled,
|
||||
agents_active,
|
||||
sess.dynamic_tools.as_slice(),
|
||||
);
|
||||
|
||||
// Start a new scratchpad for this HTTP attempt
|
||||
@@ -3056,6 +3060,9 @@ async fn handle_function_call(
|
||||
"kill" => handle_kill(sess, &ctx, arguments).await,
|
||||
"code_bridge" | "code_bridge_subscription" => handle_code_bridge(sess, &ctx, arguments).await,
|
||||
_ => {
|
||||
if sess.is_dynamic_tool(&name) {
|
||||
return handle_dynamic_tool_call(sess, &ctx, name, arguments).await;
|
||||
}
|
||||
match sess.mcp_connection_manager.parse_tool_name(&name) {
|
||||
Some((server, tool_name)) => {
|
||||
// Tool timeouts are derived from per-server config; no per-call override here.
|
||||
@@ -3165,6 +3172,76 @@ async fn handle_request_user_input(
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_dynamic_tool_call(
|
||||
sess: &Session,
|
||||
ctx: &ToolCallCtx,
|
||||
tool_name: String,
|
||||
arguments: String,
|
||||
) -> ResponseInputItem {
|
||||
let args = if arguments.trim().is_empty() {
|
||||
serde_json::Value::Object(serde_json::Map::new())
|
||||
} else {
|
||||
match serde_json::from_str::<serde_json::Value>(&arguments) {
|
||||
Ok(args) => args,
|
||||
Err(err) => {
|
||||
return ResponseInputItem::FunctionCallOutput {
|
||||
call_id: ctx.call_id.clone(),
|
||||
output: FunctionCallOutputPayload {
|
||||
content: format!("invalid dynamic tool arguments: {err}"),
|
||||
success: Some(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let rx_response = match sess.register_pending_dynamic_tool(ctx.call_id.clone()) {
|
||||
Ok(rx) => rx,
|
||||
Err(err) => {
|
||||
return ResponseInputItem::FunctionCallOutput {
|
||||
call_id: ctx.call_id.clone(),
|
||||
output: FunctionCallOutputPayload {
|
||||
content: err,
|
||||
success: Some(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
sess.send_ordered_from_ctx(
|
||||
ctx,
|
||||
EventMsg::DynamicToolCallRequest(code_protocol::dynamic_tools::DynamicToolCallRequest {
|
||||
call_id: ctx.call_id.clone(),
|
||||
turn_id: ctx.sub_id.clone(),
|
||||
tool: tool_name,
|
||||
arguments: args,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = match rx_response.await {
|
||||
Ok(response) => response,
|
||||
Err(_) => {
|
||||
return ResponseInputItem::FunctionCallOutput {
|
||||
call_id: ctx.call_id.clone(),
|
||||
output: FunctionCallOutputPayload {
|
||||
content: "dynamic tool call was cancelled before receiving a response"
|
||||
.to_string(),
|
||||
success: Some(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id: ctx.call_id.clone(),
|
||||
output: FunctionCallOutputPayload {
|
||||
content: response.output,
|
||||
success: Some(response.success),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_browser_cleanup(sess: &Session, ctx: &ToolCallCtx) -> ResponseInputItem {
|
||||
let call_id_clone = ctx.call_id.clone();
|
||||
let _sess_clone = sess;
|
||||
|
||||
@@ -42,6 +42,7 @@ use crate::config_types::ReasoningSummary;
|
||||
use crate::project_features::{load_project_commands, ProjectCommand, ProjectHooks};
|
||||
use code_app_server_protocol::AuthMode;
|
||||
use code_protocol::config_types::SandboxMode;
|
||||
use code_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use std::time::Instant;
|
||||
use serde::Deserialize;
|
||||
use serde::de::{self, Unexpected};
|
||||
@@ -291,6 +292,9 @@ pub struct Config {
|
||||
/// Optional ACP client tool identifiers supplied by the host IDE.
|
||||
pub experimental_client_tools: Option<ClientTools>,
|
||||
|
||||
/// Dynamic tool specifications injected by the client.
|
||||
pub dynamic_tools: Vec<DynamicToolSpec>,
|
||||
|
||||
/// Configuration for available agent models
|
||||
pub agents: Vec<AgentConfig>,
|
||||
|
||||
@@ -553,6 +557,10 @@ pub struct ConfigToml {
|
||||
#[serde(default)]
|
||||
pub experimental_client_tools: Option<ClientTools>,
|
||||
|
||||
/// Dynamic tool specifications injected by the client.
|
||||
#[serde(default)]
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
|
||||
/// Configuration for available agent models
|
||||
#[serde(default)]
|
||||
pub agents: Vec<AgentConfig>,
|
||||
@@ -844,6 +852,7 @@ pub struct ConfigOverrides {
|
||||
pub tools_web_search_request: Option<bool>,
|
||||
pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
|
||||
pub experimental_client_tools: Option<ClientTools>,
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
pub compact_prompt_override: Option<String>,
|
||||
pub compact_prompt_override_file: Option<PathBuf>,
|
||||
}
|
||||
@@ -881,6 +890,7 @@ impl Config {
|
||||
tools_web_search_request: override_tools_web_search_request,
|
||||
mcp_servers,
|
||||
experimental_client_tools,
|
||||
dynamic_tools,
|
||||
compact_prompt_override,
|
||||
compact_prompt_override_file,
|
||||
} = overrides;
|
||||
@@ -893,6 +903,10 @@ impl Config {
|
||||
cfg.experimental_client_tools = Some(client_tools);
|
||||
}
|
||||
|
||||
if let Some(dynamic_tools) = dynamic_tools {
|
||||
cfg.dynamic_tools = Some(dynamic_tools);
|
||||
}
|
||||
|
||||
let (active_profile_name, config_profile) =
|
||||
match config_profile_key.as_ref().or(cfg.profile.as_ref()) {
|
||||
Some(key) => {
|
||||
@@ -1376,6 +1390,7 @@ impl Config {
|
||||
compact_prompt_override,
|
||||
mcp_servers: cfg.mcp_servers,
|
||||
experimental_client_tools: cfg.experimental_client_tools.clone(),
|
||||
dynamic_tools: cfg.dynamic_tools.unwrap_or_default(),
|
||||
agents,
|
||||
model_providers,
|
||||
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES),
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::model_family::ModelFamily;
|
||||
use crate::plan_tool::PLAN_TOOL;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use code_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use crate::tool_apply_patch::{
|
||||
create_apply_patch_freeform_tool, create_apply_patch_json_tool, ApplyPatchToolType,
|
||||
};
|
||||
@@ -676,6 +677,25 @@ pub(crate) fn mcp_tool_to_openai_tool(
|
||||
})
|
||||
}
|
||||
|
||||
fn dynamic_tool_to_openai_tool(
|
||||
tool: &DynamicToolSpec,
|
||||
) -> Result<ResponsesApiTool, serde_json::Error> {
|
||||
let input_schema = parse_tool_input_schema(&tool.input_schema)?;
|
||||
|
||||
Ok(ResponsesApiTool {
|
||||
name: tool.name.clone(),
|
||||
description: tool.description.clone(),
|
||||
strict: false,
|
||||
parameters: input_schema,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_tool_input_schema(input_schema: &JsonValue) -> Result<JsonSchema, serde_json::Error> {
|
||||
let mut input_schema = input_schema.clone();
|
||||
sanitize_json_schema(&mut input_schema);
|
||||
serde_json::from_value::<JsonSchema>(input_schema)
|
||||
}
|
||||
|
||||
/// Sanitize a JSON Schema (as serde_json::Value) so it can fit our limited
|
||||
/// JsonSchema enum. This function:
|
||||
/// - Ensures every schema object has a "type". If missing, infers it from
|
||||
@@ -795,6 +815,7 @@ pub fn get_openai_tools(
|
||||
mcp_tools: Option<HashMap<String, mcp_types::Tool>>,
|
||||
browser_enabled: bool,
|
||||
_agents_active: bool,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> Vec<OpenAiTool> {
|
||||
let mut tools: Vec<OpenAiTool> = Vec::new();
|
||||
|
||||
@@ -876,6 +897,20 @@ pub fn get_openai_tools(
|
||||
}
|
||||
}
|
||||
|
||||
if !dynamic_tools.is_empty() {
|
||||
for tool in dynamic_tools {
|
||||
match dynamic_tool_to_openai_tool(tool) {
|
||||
Ok(converted_tool) => tools.push(OpenAiTool::Function(converted_tool)),
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to convert dynamic tool {:?} to OpenAI tool: {e:?}",
|
||||
tool.name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
@@ -1106,7 +1141,7 @@ mod tests {
|
||||
false,
|
||||
);
|
||||
apply_default_agent_models(&mut config);
|
||||
let tools = get_openai_tools(&config, Some(HashMap::new()), false, false);
|
||||
let tools = get_openai_tools(&config, Some(HashMap::new()), false, false, &[]);
|
||||
|
||||
assert_eq_tool_names(
|
||||
&tools,
|
||||
@@ -1140,7 +1175,7 @@ mod tests {
|
||||
false,
|
||||
);
|
||||
apply_default_agent_models(&mut config);
|
||||
let tools = get_openai_tools(&config, Some(HashMap::new()), false, true);
|
||||
let tools = get_openai_tools(&config, Some(HashMap::new()), false, true, &[]);
|
||||
|
||||
assert_eq_tool_names(
|
||||
&tools,
|
||||
@@ -1173,7 +1208,7 @@ mod tests {
|
||||
false,
|
||||
);
|
||||
apply_default_agent_models(&mut config);
|
||||
let tools = get_openai_tools(&config, Some(HashMap::new()), false, false);
|
||||
let tools = get_openai_tools(&config, Some(HashMap::new()), false, false, &[]);
|
||||
|
||||
assert_eq_tool_names(
|
||||
&tools,
|
||||
@@ -1244,6 +1279,7 @@ mod tests {
|
||||
)])),
|
||||
false,
|
||||
true,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_eq_tool_names(
|
||||
@@ -1367,6 +1403,7 @@ mod tests {
|
||||
)])),
|
||||
false,
|
||||
true,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_eq_tool_names(
|
||||
@@ -1493,6 +1530,7 @@ mod tests {
|
||||
)])),
|
||||
false,
|
||||
true,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_eq_tool_names(
|
||||
@@ -1569,6 +1607,7 @@ mod tests {
|
||||
)])),
|
||||
false,
|
||||
true,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_eq_tool_names(
|
||||
@@ -1645,6 +1684,7 @@ mod tests {
|
||||
)])),
|
||||
false,
|
||||
true,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_eq_tool_names(
|
||||
@@ -1719,6 +1759,7 @@ mod tests {
|
||||
)])),
|
||||
false,
|
||||
true,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_eq_tool_names(
|
||||
|
||||
@@ -27,7 +27,9 @@ use crate::model_provider_info::ModelProviderInfo;
|
||||
use crate::client_common::TextFormat;
|
||||
use crate::parse_command::ParsedCommand;
|
||||
use crate::plan_tool::UpdatePlanArgs;
|
||||
use code_protocol::dynamic_tools::DynamicToolCallRequest;
|
||||
use code_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use code_protocol::dynamic_tools::DynamicToolSpec;
|
||||
|
||||
// Re-export review types from the shared protocol crate so callers can use
|
||||
// `code_core::protocol::ReviewFinding` and friends.
|
||||
@@ -139,6 +141,10 @@ pub enum Op {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
demo_developer_message: Option<String>,
|
||||
|
||||
/// Dynamic tools to include for this session.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
},
|
||||
|
||||
/// Abort current task.
|
||||
@@ -816,6 +822,8 @@ pub enum EventMsg {
|
||||
|
||||
RequestUserInput(RequestUserInputEvent),
|
||||
|
||||
DynamicToolCallRequest(DynamicToolCallRequest),
|
||||
|
||||
ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
|
||||
|
||||
BackgroundEvent(BackgroundEventEvent),
|
||||
|
||||
@@ -209,6 +209,14 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
"Model requested user input ({question_count} question(s)); interactive prompts are not supported in this runner.",
|
||||
);
|
||||
}
|
||||
EventMsg::DynamicToolCallRequest(ev) => {
|
||||
let tool = &ev.tool;
|
||||
let call_id = &ev.call_id;
|
||||
ts_println!(
|
||||
self,
|
||||
"Dynamic tool call requested for {tool} (call_id: {call_id}); dynamic tools are not supported in this runner.",
|
||||
);
|
||||
}
|
||||
EventMsg::EnvironmentContextFull(_)
|
||||
| EventMsg::EnvironmentContextDelta(_)
|
||||
| EventMsg::BrowserSnapshot(_)
|
||||
|
||||
@@ -289,6 +289,7 @@ pub async fn run_main(cli: Cli, code_linux_sandbox_exe: Option<PathBuf>) -> anyh
|
||||
tools_web_search_request: None,
|
||||
mcp_servers: None,
|
||||
experimental_client_tools: None,
|
||||
dynamic_tools: None,
|
||||
compact_prompt_override: None,
|
||||
compact_prompt_override_file: None,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ use agent_client_protocol as acp;
|
||||
use code_core::config_types::ClientTools;
|
||||
use code_core::protocol::AskForApproval;
|
||||
use code_protocol::config_types::SandboxMode;
|
||||
use code_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use code_utils_json_to_toml::json_to_toml;
|
||||
use mcp_types::Tool;
|
||||
use mcp_types::ToolInputSchema;
|
||||
@@ -57,6 +58,10 @@ pub struct CodexToolCallParam {
|
||||
/// Whether to include the plan tool in the conversation.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub include_plan_tool: Option<bool>,
|
||||
|
||||
/// Dynamic tool specifications injected by the client.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
}
|
||||
|
||||
/// Custom enum mirroring [`AskForApproval`], but has an extra dependency on
|
||||
@@ -248,6 +253,7 @@ impl CodexToolCallParam {
|
||||
config: cli_overrides,
|
||||
base_instructions,
|
||||
include_plan_tool,
|
||||
dynamic_tools,
|
||||
} = self;
|
||||
|
||||
// Build the `ConfigOverrides` recognized by codex-core.
|
||||
@@ -270,6 +276,7 @@ impl CodexToolCallParam {
|
||||
tools_web_search_request: None,
|
||||
mcp_servers: None,
|
||||
experimental_client_tools: None,
|
||||
dynamic_tools,
|
||||
compact_prompt_override: None,
|
||||
compact_prompt_override_file: None,
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ use code_core::protocol::InputItem;
|
||||
use code_core::protocol::Op;
|
||||
use code_core::protocol::Submission;
|
||||
use code_core::protocol::TaskCompleteEvent;
|
||||
use code_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use mcp_types::CallToolResult;
|
||||
use mcp_types::ContentBlock;
|
||||
use mcp_types::RequestId;
|
||||
@@ -252,6 +253,23 @@ async fn run_code_tool_session_inner(
|
||||
.remove(&request_id);
|
||||
break;
|
||||
}
|
||||
EventMsg::DynamicToolCallRequest(ev) => {
|
||||
let response = DynamicToolResponse {
|
||||
call_id: ev.call_id.clone(),
|
||||
output: "dynamic tools are not supported in MCP tool sessions"
|
||||
.to_string(),
|
||||
success: false,
|
||||
};
|
||||
if let Err(err) = codex
|
||||
.submit(Op::DynamicToolResponse {
|
||||
id: ev.call_id,
|
||||
response,
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::error!("failed to submit DynamicToolResponse: {err}");
|
||||
}
|
||||
}
|
||||
EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {
|
||||
let text = match last_agent_message {
|
||||
Some(msg) => msg.clone(),
|
||||
|
||||
@@ -1630,6 +1630,7 @@ fn configure_session_op_from_config(config: &Config) -> Op {
|
||||
cwd: config.cwd.clone(),
|
||||
resume_path: None,
|
||||
demo_developer_message: config.demo_developer_message.clone(),
|
||||
dynamic_tools: config.dynamic_tools.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ pub(crate) async fn handle_create_conversation(
|
||||
tools_web_search_request: None,
|
||||
mcp_servers: None,
|
||||
experimental_client_tools: None,
|
||||
dynamic_tools: None,
|
||||
compact_prompt_override: None,
|
||||
compact_prompt_override_file: None,
|
||||
};
|
||||
|
||||
@@ -47,6 +47,8 @@ pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
|
||||
code_app_server_protocol::ApplyPatchApprovalResponse::export_all_to(out_dir)?;
|
||||
code_app_server_protocol::ExecCommandApprovalParams::export_all_to(out_dir)?;
|
||||
code_app_server_protocol::ExecCommandApprovalResponse::export_all_to(out_dir)?;
|
||||
code_app_server_protocol::DynamicToolCallParams::export_all_to(out_dir)?;
|
||||
code_app_server_protocol::DynamicToolCallResponse::export_all_to(out_dir)?;
|
||||
code_app_server_protocol::ServerNotification::export_all_to(out_dir)?;
|
||||
code_app_server_protocol::ClientNotification::export_all_to(out_dir)?;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::config_types::ReasoningEffort;
|
||||
use crate::config_types::ReasoningSummary;
|
||||
use crate::config_types::SandboxMode;
|
||||
use crate::config_types::Verbosity;
|
||||
use crate::dynamic_tools::DynamicToolSpec;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::EventMsg;
|
||||
use crate::protocol::FileChange;
|
||||
@@ -297,6 +298,10 @@ pub struct NewConversationParams {
|
||||
/// Whether to include the apply patch tool in the conversation.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include_apply_patch_tool: Option<bool>,
|
||||
|
||||
/// Dynamic tool specifications injected by the client.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||||
@@ -669,6 +674,7 @@ pub enum InputItem {
|
||||
|
||||
pub const APPLY_PATCH_APPROVAL_METHOD: &str = "applyPatchApproval";
|
||||
pub const EXEC_COMMAND_APPROVAL_METHOD: &str = "execCommandApproval";
|
||||
pub const DYNAMIC_TOOL_CALL_METHOD: &str = "dynamicToolCall";
|
||||
|
||||
/// Request initiated from the server and sent to the client.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||||
@@ -686,6 +692,12 @@ pub enum ServerRequest {
|
||||
request_id: RequestId,
|
||||
params: ExecCommandApprovalParams,
|
||||
},
|
||||
/// Request to execute a dynamic tool.
|
||||
DynamicToolCall {
|
||||
#[serde(rename = "id")]
|
||||
request_id: RequestId,
|
||||
params: DynamicToolCallParams,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||||
@@ -721,6 +733,21 @@ pub struct ExecCommandApprovalResponse {
|
||||
pub decision: ReviewDecision,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||||
pub struct DynamicToolCallParams {
|
||||
pub conversation_id: ConversationId,
|
||||
pub turn_id: String,
|
||||
pub call_id: String,
|
||||
pub tool: String,
|
||||
pub arguments: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||||
pub struct DynamicToolCallResponse {
|
||||
pub output: String,
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
|
||||
pub struct ApplyPatchApprovalResponse {
|
||||
pub decision: ReviewDecision,
|
||||
@@ -859,6 +886,7 @@ mod tests {
|
||||
base_instructions: None,
|
||||
include_plan_tool: None,
|
||||
include_apply_patch_tool: None,
|
||||
dynamic_tools: None,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
|
||||
@@ -55,6 +55,7 @@ use code_core::auth_accounts::{self, StoredAccount};
|
||||
use code_login::AuthManager;
|
||||
use code_login::AuthMode;
|
||||
use code_protocol::mcp_protocol::AuthMode as McpAuthMode;
|
||||
use code_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use code_protocol::protocol::SessionSource;
|
||||
use code_protocol::num_format::format_with_separators;
|
||||
use code_core::split_command_and_args;
|
||||
@@ -13976,6 +13977,42 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
EventMsg::DynamicToolCallRequest(ev) => {
|
||||
let key = self.near_time_key_current_req(event.order.as_ref());
|
||||
let tool = &ev.tool;
|
||||
let call_id = &ev.call_id;
|
||||
let lines = vec![
|
||||
format!("Dynamic tool call requested: {tool}"),
|
||||
format!("call_id: {call_id}"),
|
||||
"Dynamic tools are not supported in this UI; returning a failure response."
|
||||
.to_string(),
|
||||
];
|
||||
let role = history_cell::plain_role_for_kind(PlainMessageKind::Notice);
|
||||
let state = history_cell::plain_message_state_from_paragraphs(
|
||||
PlainMessageKind::Notice,
|
||||
role,
|
||||
lines,
|
||||
);
|
||||
let _ = self.history_insert_plain_state_with_key(state, key, "dynamic_tool_call");
|
||||
self.restore_reasoning_in_progress_if_streaming();
|
||||
|
||||
let response = DynamicToolResponse {
|
||||
call_id: ev.call_id.clone(),
|
||||
output: "dynamic tools are not supported in this UI".to_string(),
|
||||
success: false,
|
||||
};
|
||||
if let Err(e) = self.code_op_tx.send(Op::DynamicToolResponse {
|
||||
id: ev.call_id.clone(),
|
||||
response,
|
||||
}) {
|
||||
tracing::error!("failed to send Op::DynamicToolResponse: {e}");
|
||||
}
|
||||
|
||||
self.bottom_pane
|
||||
.update_status_text("waiting for model".to_string());
|
||||
self.bottom_pane.set_task_running(true);
|
||||
self.request_redraw();
|
||||
}
|
||||
EventMsg::ApplyPatchApprovalRequest(ev) => {
|
||||
let id2 = id.clone();
|
||||
let ev2 = ev.clone();
|
||||
@@ -22401,6 +22438,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
cwd: self.config.cwd.clone(),
|
||||
resume_path: None,
|
||||
demo_developer_message: self.config.demo_developer_message.clone(),
|
||||
dynamic_tools: Vec::new(),
|
||||
};
|
||||
self.submit_op(op);
|
||||
|
||||
@@ -23031,6 +23069,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
cwd: self.config.cwd.clone(),
|
||||
resume_path: None,
|
||||
demo_developer_message: self.config.demo_developer_message.clone(),
|
||||
dynamic_tools: Vec::new(),
|
||||
};
|
||||
self.submit_op(op);
|
||||
}
|
||||
@@ -23057,6 +23096,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
cwd: self.config.cwd.clone(),
|
||||
resume_path: None,
|
||||
demo_developer_message: self.config.demo_developer_message.clone(),
|
||||
dynamic_tools: Vec::new(),
|
||||
};
|
||||
self.submit_op(op);
|
||||
}
|
||||
@@ -23263,6 +23303,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
cwd: self.config.cwd.clone(),
|
||||
resume_path: None,
|
||||
demo_developer_message: self.config.demo_developer_message.clone(),
|
||||
dynamic_tools: Vec::new(),
|
||||
};
|
||||
let _ = self.code_op_tx.send(op);
|
||||
} else {
|
||||
@@ -23407,6 +23448,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
cwd: self.config.cwd.clone(),
|
||||
resume_path: None,
|
||||
demo_developer_message: self.config.demo_developer_message.clone(),
|
||||
dynamic_tools: Vec::new(),
|
||||
};
|
||||
|
||||
self.submit_op(op);
|
||||
@@ -23448,6 +23490,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
cwd: self.config.cwd.clone(),
|
||||
resume_path: None,
|
||||
demo_developer_message: self.config.demo_developer_message.clone(),
|
||||
dynamic_tools: Vec::new(),
|
||||
};
|
||||
|
||||
self.submit_op(op);
|
||||
@@ -24637,6 +24680,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
cwd: self.config.cwd.clone(),
|
||||
resume_path: None,
|
||||
demo_developer_message: self.config.demo_developer_message.clone(),
|
||||
dynamic_tools: Vec::new(),
|
||||
};
|
||||
self.submit_op(op);
|
||||
|
||||
@@ -36687,6 +36731,7 @@ impl ChatWidget<'_> {
|
||||
cwd: self.config.cwd.clone(),
|
||||
resume_path: None,
|
||||
demo_developer_message: self.config.demo_developer_message.clone(),
|
||||
dynamic_tools: Vec::new(),
|
||||
};
|
||||
self.submit_op(op);
|
||||
|
||||
|
||||
@@ -453,6 +453,7 @@ pub async fn run_main(
|
||||
tools_web_search_request: Some(cli.web_search),
|
||||
mcp_servers: None,
|
||||
experimental_client_tools: None,
|
||||
dynamic_tools: None,
|
||||
compact_prompt_override: cli.compact_prompt_override.clone(),
|
||||
compact_prompt_override_file: cli.compact_prompt_file.clone(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user