mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
68a1d82a41
## Summary Stop counting elicitation time towards mcp tool call time. There are some tradeoffs here, but in general I don't think time spent waiting for elicitations should count towards tool call time, or at least not directly towards timeouts. Elicitations are not exactly like exec_command escalation requests, but I would argue it's ~roughly equivalent. ## Testing - [x] Added unit tests - [x] Tested locally
222 lines
6.7 KiB
Rust
222 lines
6.7 KiB
Rust
use std::sync::Arc;
|
|
|
|
use rmcp::RoleClient;
|
|
use rmcp::model::ClientInfo;
|
|
use rmcp::model::ClientResult;
|
|
use rmcp::model::CustomResult;
|
|
use rmcp::model::ElicitationAction;
|
|
use rmcp::model::Meta;
|
|
use rmcp::model::RequestParamsMeta;
|
|
use rmcp::model::ServerNotification;
|
|
use rmcp::model::ServerRequest;
|
|
use rmcp::service::NotificationContext;
|
|
use rmcp::service::RequestContext;
|
|
use rmcp::service::Service;
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
|
|
use crate::logging_client_handler::LoggingClientHandler;
|
|
use crate::rmcp_client::Elicitation;
|
|
use crate::rmcp_client::ElicitationPauseState;
|
|
use crate::rmcp_client::ElicitationResponse;
|
|
use crate::rmcp_client::SendElicitation;
|
|
|
|
const MCP_PROGRESS_TOKEN_META_KEY: &str = "progressToken";
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct ElicitationClientService {
|
|
handler: LoggingClientHandler,
|
|
send_elicitation: Arc<SendElicitation>,
|
|
pause_state: ElicitationPauseState,
|
|
}
|
|
|
|
impl ElicitationClientService {
|
|
pub(crate) fn new(
|
|
client_info: ClientInfo,
|
|
send_elicitation: SendElicitation,
|
|
pause_state: ElicitationPauseState,
|
|
) -> Self {
|
|
let send_elicitation = Arc::new(send_elicitation);
|
|
Self {
|
|
handler: LoggingClientHandler::new(
|
|
client_info,
|
|
clone_send_elicitation(Arc::clone(&send_elicitation)),
|
|
),
|
|
send_elicitation,
|
|
pause_state,
|
|
}
|
|
}
|
|
|
|
async fn create_elicitation(
|
|
&self,
|
|
request: Elicitation,
|
|
context: RequestContext<RoleClient>,
|
|
) -> Result<ElicitationResponse, rmcp::ErrorData> {
|
|
let RequestContext { id, meta, .. } = context;
|
|
let request = restore_context_meta(request, meta);
|
|
let _pause = self.pause_state.enter();
|
|
(self.send_elicitation)(id, request)
|
|
.await
|
|
.map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))
|
|
}
|
|
}
|
|
|
|
fn clone_send_elicitation(send_elicitation: Arc<SendElicitation>) -> SendElicitation {
|
|
Box::new(move |request_id, request| send_elicitation(request_id, request))
|
|
}
|
|
|
|
impl Service<RoleClient> for ElicitationClientService {
|
|
async fn handle_request(
|
|
&self,
|
|
request: ServerRequest,
|
|
context: RequestContext<RoleClient>,
|
|
) -> Result<ClientResult, rmcp::ErrorData> {
|
|
match request {
|
|
ServerRequest::CreateElicitationRequest(request) => {
|
|
let response = self.create_elicitation(request.params, context).await?;
|
|
// RMCP's typed CreateElicitationResult does not model result-level `_meta`.
|
|
let result = elicitation_response_result(response)?;
|
|
Ok(ClientResult::CustomResult(result))
|
|
}
|
|
request => {
|
|
<LoggingClientHandler as Service<RoleClient>>::handle_request(
|
|
&self.handler,
|
|
request,
|
|
context,
|
|
)
|
|
.await
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn handle_notification(
|
|
&self,
|
|
notification: ServerNotification,
|
|
context: NotificationContext<RoleClient>,
|
|
) -> Result<(), rmcp::ErrorData> {
|
|
<LoggingClientHandler as Service<RoleClient>>::handle_notification(
|
|
&self.handler,
|
|
notification,
|
|
context,
|
|
)
|
|
.await
|
|
}
|
|
|
|
fn get_info(&self) -> ClientInfo {
|
|
<LoggingClientHandler as Service<RoleClient>>::get_info(&self.handler)
|
|
}
|
|
}
|
|
|
|
fn restore_context_meta(mut request: Elicitation, mut context_meta: Meta) -> Elicitation {
|
|
// RMCP lifts JSON-RPC `_meta` into RequestContext before invoking services.
|
|
context_meta.remove(MCP_PROGRESS_TOKEN_META_KEY);
|
|
if context_meta.is_empty() {
|
|
return request;
|
|
}
|
|
|
|
request
|
|
.meta_mut()
|
|
.get_or_insert_with(Meta::new)
|
|
.extend(context_meta);
|
|
request
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct CreateElicitationResultWithMeta {
|
|
action: ElicitationAction,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
content: Option<Value>,
|
|
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
|
meta: Option<Value>,
|
|
}
|
|
|
|
fn elicitation_response_result(
|
|
response: ElicitationResponse,
|
|
) -> Result<CustomResult, rmcp::ErrorData> {
|
|
let ElicitationResponse {
|
|
action,
|
|
content,
|
|
meta,
|
|
} = response;
|
|
let result = CreateElicitationResultWithMeta {
|
|
action,
|
|
content,
|
|
meta,
|
|
};
|
|
|
|
serde_json::to_value(result)
|
|
.map(CustomResult)
|
|
.map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use pretty_assertions::assert_eq;
|
|
use rmcp::model::BooleanSchema;
|
|
use rmcp::model::CreateElicitationRequestParams;
|
|
use rmcp::model::ElicitationSchema;
|
|
use rmcp::model::PrimitiveSchema;
|
|
use serde_json::Value;
|
|
use serde_json::json;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn restore_context_meta_adds_elicitation_meta_and_removes_progress_token() {
|
|
let request = restore_context_meta(
|
|
form_request(/*meta*/ None),
|
|
meta(json!({
|
|
"progressToken": "progress-token",
|
|
"persist": ["session", "always"],
|
|
})),
|
|
);
|
|
|
|
assert_eq!(
|
|
request,
|
|
form_request(Some(meta(json!({
|
|
"persist": ["session", "always"],
|
|
}))))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn elicitation_response_result_serializes_response_meta() {
|
|
let result = rmcp::model::ClientResult::CustomResult(
|
|
elicitation_response_result(ElicitationResponse {
|
|
action: ElicitationAction::Accept,
|
|
content: Some(json!({ "confirmed": true })),
|
|
meta: Some(json!({ "persist": "always" })),
|
|
})
|
|
.expect("elicitation response should serialize"),
|
|
);
|
|
|
|
assert_eq!(
|
|
serde_json::to_value(result).expect("client result should serialize"),
|
|
json!({
|
|
"action": "accept",
|
|
"content": { "confirmed": true },
|
|
"_meta": { "persist": "always" },
|
|
})
|
|
);
|
|
}
|
|
|
|
fn form_request(meta: Option<Meta>) -> CreateElicitationRequestParams {
|
|
CreateElicitationRequestParams::FormElicitationParams {
|
|
meta,
|
|
message: "Confirm?".to_string(),
|
|
requested_schema: ElicitationSchema::builder()
|
|
.required_property("confirmed", PrimitiveSchema::Boolean(BooleanSchema::new()))
|
|
.build()
|
|
.expect("schema should build"),
|
|
}
|
|
}
|
|
|
|
fn meta(value: Value) -> Meta {
|
|
let Value::Object(map) = value else {
|
|
panic!("meta must be an object");
|
|
};
|
|
Meta(map)
|
|
}
|
|
}
|