mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
Compare commits
1 Commits
dev/jm/fix
...
anton_pana
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9662e46280 |
3
codex-rs/Cargo.lock
generated
3
codex-rs/Cargo.lock
generated
@@ -1964,6 +1964,8 @@ dependencies = [
|
||||
"codex-utils-cargo-bin",
|
||||
"inventory",
|
||||
"pretty_assertions",
|
||||
"prost 0.14.3",
|
||||
"prost-types 0.14.3",
|
||||
"rmcp",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
@@ -1973,6 +1975,7 @@ dependencies = [
|
||||
"strum_macros 0.28.0",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tonic-prost-build",
|
||||
"tracing",
|
||||
"ts-rs",
|
||||
"uuid",
|
||||
|
||||
@@ -949,6 +949,7 @@ mod tests {
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_app_server_protocol::ToolRequestUserInputQuestion;
|
||||
use codex_app_server_protocol::proto::jsonrpc;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
@@ -1086,9 +1087,8 @@ mod tests {
|
||||
return serde_json::from_str::<JSONRPCMessage>(&text)
|
||||
.expect("text frame should be valid JSON-RPC");
|
||||
}
|
||||
Message::Binary(_) | Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => {
|
||||
continue;
|
||||
}
|
||||
Message::Binary(_) => panic!("remote client should write text JSON-RPC frames"),
|
||||
Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => continue,
|
||||
Message::Close(_) => panic!("unexpected close frame"),
|
||||
}
|
||||
}
|
||||
@@ -1108,6 +1108,18 @@ mod tests {
|
||||
.expect("message should send");
|
||||
}
|
||||
|
||||
async fn write_websocket_binary_message(
|
||||
websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
|
||||
message: JSONRPCMessage,
|
||||
) {
|
||||
websocket
|
||||
.send(Message::Binary(
|
||||
jsonrpc::encode_jsonrpc_message(message).into(),
|
||||
))
|
||||
.await
|
||||
.expect("binary message should send");
|
||||
}
|
||||
|
||||
fn command_execution_output_delta_notification(delta: &str) -> ServerNotification {
|
||||
ServerNotification::CommandExecutionOutputDelta(
|
||||
codex_app_server_protocol::CommandExecutionOutputDeltaNotification {
|
||||
@@ -1400,6 +1412,54 @@ mod tests {
|
||||
client.shutdown().await.expect("shutdown should complete");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_typed_request_accepts_protobuf_binary_response() {
|
||||
let websocket_url = start_test_remote_server(|mut websocket| async move {
|
||||
expect_remote_initialize(&mut websocket).await;
|
||||
let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
|
||||
else {
|
||||
panic!("expected account/read request");
|
||||
};
|
||||
assert_eq!(request.method, "account/read");
|
||||
write_websocket_binary_message(
|
||||
&mut websocket,
|
||||
JSONRPCMessage::Response(JSONRPCResponse {
|
||||
id: request.id,
|
||||
result: serde_json::to_value(GetAccountResponse {
|
||||
account: None,
|
||||
requires_openai_auth: false,
|
||||
})
|
||||
.expect("response should serialize"),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
websocket.close(None).await.expect("close should succeed");
|
||||
})
|
||||
.await;
|
||||
let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
|
||||
.await
|
||||
.expect("remote client should connect");
|
||||
|
||||
let response: GetAccountResponse = client
|
||||
.request_typed(ClientRequest::GetAccount {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: codex_app_server_protocol::GetAccountParams {
|
||||
refresh_token: false,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.expect("typed request should decode binary response");
|
||||
assert_eq!(
|
||||
response,
|
||||
GetAccountResponse {
|
||||
account: None,
|
||||
requires_openai_auth: false,
|
||||
}
|
||||
);
|
||||
|
||||
client.shutdown().await.expect("shutdown should complete");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_typed_request_accepts_large_single_frame_response() {
|
||||
let padding = "x".repeat((17 << 20) + 1024);
|
||||
|
||||
@@ -35,6 +35,7 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::Result as JsonRpcResult;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::proto::jsonrpc;
|
||||
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
@@ -311,8 +312,8 @@ impl RemoteAppServerClient {
|
||||
}
|
||||
message = stream.next() => {
|
||||
match message {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
match serde_json::from_str::<JSONRPCMessage>(&text) {
|
||||
Some(Ok(message @ (Message::Text(_) | Message::Binary(_)))) => {
|
||||
match decode_websocket_jsonrpc_message(message, &websocket_url, "message") {
|
||||
Ok(JSONRPCMessage::Response(response)) => {
|
||||
if let Some(response_tx) = pending_requests.remove(&response.id) {
|
||||
let _ = response_tx.send(Ok(Ok(response.result)));
|
||||
@@ -385,9 +386,7 @@ impl RemoteAppServerClient {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let message = format!(
|
||||
"remote app server at `{websocket_url}` sent invalid JSON-RPC: {err}"
|
||||
);
|
||||
let message = err.to_string();
|
||||
let _ = deliver_event(
|
||||
&event_tx,
|
||||
AppServerEvent::Disconnected {
|
||||
@@ -421,8 +420,7 @@ impl RemoteAppServerClient {
|
||||
));
|
||||
break;
|
||||
}
|
||||
Some(Ok(Message::Binary(_)))
|
||||
| Some(Ok(Message::Ping(_)))
|
||||
Some(Ok(Message::Ping(_)))
|
||||
| Some(Ok(Message::Pong(_)))
|
||||
| Some(Ok(Message::Frame(_))) => {}
|
||||
Some(Err(err)) => {
|
||||
@@ -700,12 +698,12 @@ async fn initialize_remote_connection(
|
||||
timeout(initialize_timeout, async {
|
||||
loop {
|
||||
match stream.next().await {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
let message = serde_json::from_str::<JSONRPCMessage>(&text).map_err(|err| {
|
||||
IoError::other(format!(
|
||||
"remote app server at `{websocket_url}` sent invalid initialize response: {err}"
|
||||
))
|
||||
})?;
|
||||
Some(Ok(message @ (Message::Text(_) | Message::Binary(_)))) => {
|
||||
let message = decode_websocket_jsonrpc_message(
|
||||
message,
|
||||
websocket_url,
|
||||
"initialize response",
|
||||
)?;
|
||||
match message {
|
||||
JSONRPCMessage::Response(response) if response.id == initialize_request_id => {
|
||||
break Ok(());
|
||||
@@ -751,8 +749,7 @@ async fn initialize_remote_connection(
|
||||
JSONRPCMessage::Response(_) | JSONRPCMessage::Error(_) => {}
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Binary(_)))
|
||||
| Some(Ok(Message::Ping(_)))
|
||||
Some(Ok(Message::Ping(_)))
|
||||
| Some(Ok(Message::Pong(_)))
|
||||
| Some(Ok(Message::Frame(_))) => {}
|
||||
Some(Ok(Message::Close(frame))) => {
|
||||
@@ -826,11 +823,7 @@ fn request_id_from_client_request(request: &ClientRequest) -> RequestId {
|
||||
}
|
||||
|
||||
fn jsonrpc_request_from_client_request(request: ClientRequest) -> JSONRPCRequest {
|
||||
let value = match serde_json::to_value(request) {
|
||||
Ok(value) => value,
|
||||
Err(err) => panic!("client request should serialize: {err}"),
|
||||
};
|
||||
match serde_json::from_value(value) {
|
||||
match request.into_jsonrpc_request() {
|
||||
Ok(request) => request,
|
||||
Err(err) => panic!("client request should encode as JSON-RPC request: {err}"),
|
||||
}
|
||||
@@ -864,6 +857,28 @@ async fn write_jsonrpc_message(
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_websocket_jsonrpc_message(
|
||||
message: Message,
|
||||
websocket_url: &str,
|
||||
context: &str,
|
||||
) -> IoResult<JSONRPCMessage> {
|
||||
match message {
|
||||
Message::Text(text) => serde_json::from_str::<JSONRPCMessage>(&text).map_err(|err| {
|
||||
IoError::other(format!(
|
||||
"remote app server at `{websocket_url}` sent invalid {context}: {err}"
|
||||
))
|
||||
}),
|
||||
Message::Binary(payload) => jsonrpc::decode_jsonrpc_message(&payload).map_err(|err| {
|
||||
IoError::other(format!(
|
||||
"remote app server at `{websocket_url}` sent invalid protobuf {context}: {err}"
|
||||
))
|
||||
}),
|
||||
Message::Close(_) | Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => Err(
|
||||
IoError::new(ErrorKind::InvalidInput, "message is not a JSON-RPC payload"),
|
||||
),
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -3,5 +3,6 @@ load("//:defs.bzl", "codex_rust_crate")
|
||||
codex_rust_crate(
|
||||
name = "app-server-protocol",
|
||||
crate_name = "codex_app_server_protocol",
|
||||
compile_data = glob(["proto/**"], allow_empty = True),
|
||||
test_data_extra = glob(["schema/**"], allow_empty = True),
|
||||
)
|
||||
|
||||
@@ -22,6 +22,8 @@ schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
prost = "0.14.3"
|
||||
prost-types = "0.14.3"
|
||||
strum_macros = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
@@ -41,3 +43,4 @@ codex-utils-cargo-bin = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
similar = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tonic-prost-build = { version = "=0.14.3", default-features = false, features = ["transport"] }
|
||||
|
||||
39
codex-rs/app-server-protocol/examples/generate-proto.rs
Normal file
39
codex-rs/app-server-protocol/examples/generate-proto.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(crate_dir_arg) = std::env::args().nth(1) else {
|
||||
eprintln!("Usage: generate-proto <app-server-protocol-crate-dir>");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let crate_dir = PathBuf::from(crate_dir_arg);
|
||||
let proto_dir = crate_dir.join("proto");
|
||||
let proto_file = proto_dir.join("codex.app_server.v2.proto");
|
||||
let out_dir = crate_dir.join("src/proto");
|
||||
|
||||
std::fs::create_dir_all(&out_dir)?;
|
||||
|
||||
tonic_prost_build::configure()
|
||||
.build_client(false)
|
||||
.build_server(false)
|
||||
.out_dir(&out_dir)
|
||||
.compile_protos(&[proto_file], &[proto_dir])?;
|
||||
|
||||
normalize_generated_rust(&out_dir.join("codex.app_server.v2.rs"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_generated_rust(path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let generated = std::fs::read_to_string(path)?;
|
||||
let normalized = generated
|
||||
.replace("::prost::alloc::string::String", "String")
|
||||
.replace("::prost::alloc::vec::Vec", "Vec")
|
||||
.replace("::core::option::Option", "Option");
|
||||
|
||||
if normalized != generated {
|
||||
std::fs::write(path, normalized)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
7675
codex-rs/app-server-protocol/proto/codex.app_server.v2.proto
Normal file
7675
codex-rs/app-server-protocol/proto/codex.app_server.v2.proto
Normal file
File diff suppressed because it is too large
Load Diff
1130
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.proto_registry.json
generated
Normal file
1130
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.proto_registry.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ use crate::export_server_notification_schemas;
|
||||
use crate::export_server_param_schemas;
|
||||
use crate::export_server_response_schemas;
|
||||
use crate::export_server_responses;
|
||||
use crate::proto_registry;
|
||||
use crate::protocol::common::EXPERIMENTAL_CLIENT_METHOD_PARAM_TYPES;
|
||||
use crate::protocol::common::EXPERIMENTAL_CLIENT_METHOD_RESPONSE_TYPES;
|
||||
use crate::protocol::common::EXPERIMENTAL_CLIENT_METHODS;
|
||||
@@ -232,6 +233,13 @@ pub fn generate_json_with_experimental(out_dir: &Path, experimental_api: bool) -
|
||||
out_dir.join("codex_app_server_protocol.v2.schemas.json"),
|
||||
&flat_v2_bundle,
|
||||
)?;
|
||||
let proto_registry = proto_registry::all_descriptors()
|
||||
.filter(|descriptor| experimental_api || descriptor.experimental_reason.is_none())
|
||||
.collect::<Vec<_>>();
|
||||
write_pretty_json(
|
||||
out_dir.join("codex_app_server_protocol.proto_registry.json"),
|
||||
&proto_registry,
|
||||
)?;
|
||||
|
||||
if !experimental_api {
|
||||
filter_experimental_json_files(out_dir)?;
|
||||
@@ -2089,7 +2097,7 @@ fn index_ts_entries(paths: &[&Path], has_v2_ts: bool) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::v2;
|
||||
use crate::proto::domain as v2;
|
||||
use crate::schema_fixtures::read_schema_fixture_subtree;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod experimental_api;
|
||||
mod export;
|
||||
mod jsonrpc_lite;
|
||||
#[doc(hidden)]
|
||||
pub mod proto;
|
||||
mod protocol;
|
||||
mod schema_fixtures;
|
||||
|
||||
@@ -13,6 +15,8 @@ pub use export::generate_ts;
|
||||
pub use export::generate_ts_with_options;
|
||||
pub use export::generate_types;
|
||||
pub use jsonrpc_lite::*;
|
||||
pub use proto::domain::*;
|
||||
pub use proto::registry as proto_registry;
|
||||
pub use protocol::common::*;
|
||||
pub use protocol::event_mapping::*;
|
||||
pub use protocol::item_builders::*;
|
||||
@@ -40,7 +44,6 @@ pub use protocol::v1::Profile;
|
||||
pub use protocol::v1::SandboxSettings;
|
||||
pub use protocol::v1::Tools;
|
||||
pub use protocol::v1::UserSavedConfig;
|
||||
pub use protocol::v2::*;
|
||||
pub use schema_fixtures::SchemaFixtureOptions;
|
||||
#[doc(hidden)]
|
||||
pub use schema_fixtures::generate_typescript_schema_fixture_subtree_for_tests;
|
||||
|
||||
7
codex-rs/app-server-protocol/src/proto.rs
Normal file
7
codex-rs/app-server-protocol/src/proto.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod domain;
|
||||
pub mod json;
|
||||
pub mod jsonrpc;
|
||||
pub mod registry;
|
||||
|
||||
#[path = "proto/codex.app_server.v2.rs"]
|
||||
pub mod pb;
|
||||
10140
codex-rs/app-server-protocol/src/proto/codex.app_server.v2.rs
Normal file
10140
codex-rs/app-server-protocol/src/proto/codex.app_server.v2.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3552,8 +3552,8 @@ pub struct ThreadStartParams {
|
||||
pub model_provider: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
@@ -3705,8 +3705,8 @@ pub struct ThreadResumeParams {
|
||||
pub model_provider: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
@@ -3810,8 +3810,8 @@ pub struct ThreadForkParams {
|
||||
pub model_provider: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
@@ -4036,8 +4036,8 @@ pub struct ThreadGoalSetParams {
|
||||
pub status: Option<ThreadGoalStatus>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable, type = "number | null")]
|
||||
@@ -4100,8 +4100,8 @@ pub struct ThreadMetadataGitInfoUpdateParams {
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option"
|
||||
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option"
|
||||
)]
|
||||
#[ts(optional = nullable, type = "string | null")]
|
||||
pub sha: Option<Option<String>>,
|
||||
@@ -4110,8 +4110,8 @@ pub struct ThreadMetadataGitInfoUpdateParams {
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option"
|
||||
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option"
|
||||
)]
|
||||
#[ts(optional = nullable, type = "string | null")]
|
||||
pub branch: Option<Option<String>>,
|
||||
@@ -4120,8 +4120,8 @@ pub struct ThreadMetadataGitInfoUpdateParams {
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option"
|
||||
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option"
|
||||
)]
|
||||
#[ts(optional = nullable, type = "string | null")]
|
||||
pub origin_url: Option<Option<String>>,
|
||||
@@ -5332,8 +5332,8 @@ pub struct ThreadRealtimeStartParams {
|
||||
pub output_modality: RealtimeOutputModality,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
@@ -5568,8 +5568,8 @@ pub struct TurnStartParams {
|
||||
/// Override the service tier for this turn and subsequent turns.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
88
codex-rs/app-server-protocol/src/proto/json.rs
Normal file
88
codex-rs/app-server-protocol/src/proto/json.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use prost_types::ListValue;
|
||||
use prost_types::NullValue;
|
||||
use prost_types::Struct;
|
||||
use prost_types::Value;
|
||||
use prost_types::value;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Map;
|
||||
use serde_json::Number;
|
||||
|
||||
pub fn to_proto_json_payload<T>(payload: &T) -> Result<Value, serde_json::Error>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
serde_json::to_value(payload).map(serde_json_to_proto_value)
|
||||
}
|
||||
|
||||
pub fn from_proto_json_payload<T>(payload: Value) -> Result<T, serde_json::Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
serde_json::from_value(proto_value_to_serde_json(payload))
|
||||
}
|
||||
|
||||
pub fn serde_json_to_proto_value(value: serde_json::Value) -> Value {
|
||||
let kind = match value {
|
||||
serde_json::Value::Null => value::Kind::NullValue(NullValue::NullValue as i32),
|
||||
serde_json::Value::Bool(value) => value::Kind::BoolValue(value),
|
||||
serde_json::Value::Number(value) => value::Kind::NumberValue(value.as_f64().unwrap_or(0.0)),
|
||||
serde_json::Value::String(value) => value::Kind::StringValue(value),
|
||||
serde_json::Value::Array(values) => value::Kind::ListValue(ListValue {
|
||||
values: values.into_iter().map(serde_json_to_proto_value).collect(),
|
||||
}),
|
||||
serde_json::Value::Object(map) => value::Kind::StructValue(Struct {
|
||||
fields: map
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, serde_json_to_proto_value(value)))
|
||||
.collect(),
|
||||
}),
|
||||
};
|
||||
Value { kind: Some(kind) }
|
||||
}
|
||||
|
||||
pub fn proto_value_to_serde_json(value: Value) -> serde_json::Value {
|
||||
match value.kind {
|
||||
Some(value::Kind::NullValue(_)) | None => serde_json::Value::Null,
|
||||
Some(value::Kind::BoolValue(value)) => serde_json::Value::Bool(value),
|
||||
Some(value::Kind::NumberValue(value)) => Number::from_f64(value)
|
||||
.map(serde_json::Value::Number)
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
Some(value::Kind::StringValue(value)) => serde_json::Value::String(value),
|
||||
Some(value::Kind::ListValue(value)) => serde_json::Value::Array(
|
||||
value
|
||||
.values
|
||||
.into_iter()
|
||||
.map(proto_value_to_serde_json)
|
||||
.collect(),
|
||||
),
|
||||
Some(value::Kind::StructValue(value)) => {
|
||||
let mut map = Map::with_capacity(value.fields.len());
|
||||
for (key, value) in value.fields {
|
||||
map.insert(key, proto_value_to_serde_json(value));
|
||||
}
|
||||
serde_json::Value::Object(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn protobuf_json_value_roundtrips_nested_payloads() {
|
||||
let original = json!({
|
||||
"array": [null, true, 3.25, "text"],
|
||||
"object": {
|
||||
"nested": "value"
|
||||
}
|
||||
});
|
||||
|
||||
let value = serde_json_to_proto_value(original.clone());
|
||||
|
||||
assert_eq!(proto_value_to_serde_json(value), original);
|
||||
}
|
||||
}
|
||||
213
codex-rs/app-server-protocol/src/proto/jsonrpc.rs
Normal file
213
codex-rs/app-server-protocol/src/proto/jsonrpc.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use prost::Message;
|
||||
|
||||
use crate::JSONRPCError;
|
||||
use crate::JSONRPCErrorError;
|
||||
use crate::JSONRPCMessage;
|
||||
use crate::JSONRPCNotification;
|
||||
use crate::JSONRPCRequest;
|
||||
use crate::JSONRPCResponse;
|
||||
use crate::RequestId;
|
||||
use crate::proto::json;
|
||||
use crate::proto::pb;
|
||||
use crate::proto::pb::jsonrpc_message;
|
||||
use crate::proto::pb::request_id;
|
||||
|
||||
pub fn encode_jsonrpc_message(message: JSONRPCMessage) -> Vec<u8> {
|
||||
pb::JsonrpcMessage::from(message).encode_to_vec()
|
||||
}
|
||||
|
||||
pub fn decode_jsonrpc_message(bytes: &[u8]) -> Result<JSONRPCMessage> {
|
||||
let message = pb::JsonrpcMessage::decode(bytes).context("decode JSON-RPC protobuf message")?;
|
||||
JSONRPCMessage::try_from(message)
|
||||
}
|
||||
|
||||
impl From<RequestId> for pb::RequestId {
|
||||
fn from(id: RequestId) -> Self {
|
||||
let kind = match id {
|
||||
RequestId::String(value) => request_id::Kind::StringValue(value),
|
||||
RequestId::Integer(value) => request_id::Kind::IntegerValue(value),
|
||||
};
|
||||
Self { kind: Some(kind) }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::RequestId> for RequestId {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(id: pb::RequestId) -> Result<Self> {
|
||||
match id.kind.context("missing request ID kind")? {
|
||||
request_id::Kind::StringValue(value) => Ok(Self::String(value)),
|
||||
request_id::Kind::IntegerValue(value) => Ok(Self::Integer(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JSONRPCRequest> for pb::JsonrpcRequest {
|
||||
fn from(request: JSONRPCRequest) -> Self {
|
||||
Self {
|
||||
id: Some(request.id.into()),
|
||||
method: request.method,
|
||||
params: request.params.map(json::serde_json_to_proto_value),
|
||||
trace: request.trace.map(|trace| pb::W3cTraceContext {
|
||||
traceparent: trace.traceparent,
|
||||
tracestate: trace.tracestate,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::JsonrpcRequest> for JSONRPCRequest {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(request: pb::JsonrpcRequest) -> Result<Self> {
|
||||
Ok(Self {
|
||||
id: request
|
||||
.id
|
||||
.context("missing request ID")?
|
||||
.try_into()
|
||||
.context("decode request ID")?,
|
||||
method: request.method,
|
||||
params: request.params.map(json::proto_value_to_serde_json),
|
||||
trace: request.trace.map(|trace| W3cTraceContext {
|
||||
traceparent: trace.traceparent,
|
||||
tracestate: trace.tracestate,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JSONRPCNotification> for pb::JsonrpcNotification {
|
||||
fn from(notification: JSONRPCNotification) -> Self {
|
||||
Self {
|
||||
method: notification.method,
|
||||
params: notification.params.map(json::serde_json_to_proto_value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pb::JsonrpcNotification> for JSONRPCNotification {
|
||||
fn from(notification: pb::JsonrpcNotification) -> Self {
|
||||
Self {
|
||||
method: notification.method,
|
||||
params: notification.params.map(json::proto_value_to_serde_json),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JSONRPCResponse> for pb::JsonrpcResponse {
|
||||
fn from(response: JSONRPCResponse) -> Self {
|
||||
Self {
|
||||
id: Some(response.id.into()),
|
||||
result: Some(json::serde_json_to_proto_value(response.result)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::JsonrpcResponse> for JSONRPCResponse {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(response: pb::JsonrpcResponse) -> Result<Self> {
|
||||
Ok(Self {
|
||||
id: response
|
||||
.id
|
||||
.context("missing response ID")?
|
||||
.try_into()
|
||||
.context("decode response ID")?,
|
||||
result: response
|
||||
.result
|
||||
.map(json::proto_value_to_serde_json)
|
||||
.context("missing response result")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JSONRPCErrorError> for pb::JsonrpcErrorError {
|
||||
fn from(error: JSONRPCErrorError) -> Self {
|
||||
Self {
|
||||
code: error.code,
|
||||
data: error.data.map(json::serde_json_to_proto_value),
|
||||
message_field: error.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pb::JsonrpcErrorError> for JSONRPCErrorError {
|
||||
fn from(error: pb::JsonrpcErrorError) -> Self {
|
||||
Self {
|
||||
code: error.code,
|
||||
data: error.data.map(json::proto_value_to_serde_json),
|
||||
message: error.message_field,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JSONRPCError> for pb::JsonrpcError {
|
||||
fn from(error: JSONRPCError) -> Self {
|
||||
Self {
|
||||
error: Some(error.error.into()),
|
||||
id: Some(error.id.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::JsonrpcError> for JSONRPCError {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(error: pb::JsonrpcError) -> Result<Self> {
|
||||
Ok(Self {
|
||||
error: error
|
||||
.error
|
||||
.context("missing JSON-RPC error payload")?
|
||||
.into(),
|
||||
id: error
|
||||
.id
|
||||
.context("missing error ID")?
|
||||
.try_into()
|
||||
.context("decode error ID")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JSONRPCMessage> for pb::JsonrpcMessage {
|
||||
fn from(message: JSONRPCMessage) -> Self {
|
||||
let kind = match message {
|
||||
JSONRPCMessage::Request(request) => {
|
||||
jsonrpc_message::Kind::JsonrpcRequest(request.into())
|
||||
}
|
||||
JSONRPCMessage::Notification(notification) => {
|
||||
jsonrpc_message::Kind::JsonrpcNotification(notification.into())
|
||||
}
|
||||
JSONRPCMessage::Response(response) => {
|
||||
jsonrpc_message::Kind::JsonrpcResponse(response.into())
|
||||
}
|
||||
JSONRPCMessage::Error(error) => jsonrpc_message::Kind::JsonrpcError(error.into()),
|
||||
};
|
||||
Self { kind: Some(kind) }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::JsonrpcMessage> for JSONRPCMessage {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(message: pb::JsonrpcMessage) -> Result<Self> {
|
||||
match message
|
||||
.kind
|
||||
.ok_or_else(|| anyhow!("missing JSON-RPC message kind"))?
|
||||
{
|
||||
jsonrpc_message::Kind::JsonrpcRequest(request) => {
|
||||
Ok(Self::Request(request.try_into()?))
|
||||
}
|
||||
jsonrpc_message::Kind::JsonrpcNotification(notification) => {
|
||||
Ok(Self::Notification(notification.into()))
|
||||
}
|
||||
jsonrpc_message::Kind::JsonrpcResponse(response) => {
|
||||
Ok(Self::Response(response.try_into()?))
|
||||
}
|
||||
jsonrpc_message::Kind::JsonrpcError(error) => Ok(Self::Error(error.try_into()?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
170
codex-rs/app-server-protocol/src/proto/registry.rs
Normal file
170
codex-rs/app-server-protocol/src/proto/registry.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
const PROTO_SOURCE: &str = include_str!("../../proto/codex.app_server.v2.proto");
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum RpcService {
|
||||
ClientToServer,
|
||||
ServerToClient,
|
||||
ClientNotifications,
|
||||
ServerNotifications,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RpcDescriptor {
|
||||
pub service: RpcService,
|
||||
pub variant: String,
|
||||
pub jsonrpc_method: String,
|
||||
pub params_type: String,
|
||||
pub response_type: String,
|
||||
pub experimental_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProtoRegistry {
|
||||
rpcs: Vec<RpcDescriptor>,
|
||||
}
|
||||
|
||||
static REGISTRY: OnceLock<ProtoRegistry> = OnceLock::new();
|
||||
|
||||
pub fn descriptors(service: RpcService) -> impl Iterator<Item = &'static RpcDescriptor> {
|
||||
registry()
|
||||
.rpcs
|
||||
.iter()
|
||||
.filter(move |descriptor| descriptor.service == service)
|
||||
}
|
||||
|
||||
pub fn all_descriptors() -> impl Iterator<Item = &'static RpcDescriptor> {
|
||||
registry().rpcs.iter()
|
||||
}
|
||||
|
||||
pub fn descriptor_by_method(
|
||||
service: RpcService,
|
||||
jsonrpc_method: &str,
|
||||
) -> Option<&'static RpcDescriptor> {
|
||||
descriptors(service).find(|descriptor| descriptor.jsonrpc_method == jsonrpc_method)
|
||||
}
|
||||
|
||||
pub fn descriptor_by_variant(service: RpcService, variant: &str) -> Option<&'static RpcDescriptor> {
|
||||
descriptors(service).find(|descriptor| descriptor.variant == variant)
|
||||
}
|
||||
|
||||
pub fn proto_source() -> &'static str {
|
||||
PROTO_SOURCE
|
||||
}
|
||||
|
||||
fn registry() -> &'static ProtoRegistry {
|
||||
REGISTRY.get_or_init(|| ProtoRegistry {
|
||||
rpcs: parse_proto_registry(PROTO_SOURCE),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_proto_registry(source: &str) -> Vec<RpcDescriptor> {
|
||||
let mut service = None;
|
||||
let mut current: Option<RpcDescriptor> = None;
|
||||
let mut descriptors = Vec::new();
|
||||
|
||||
for raw_line in source.lines() {
|
||||
let line = raw_line.trim();
|
||||
match line {
|
||||
"service ClientToServer {" => {
|
||||
service = Some(RpcService::ClientToServer);
|
||||
continue;
|
||||
}
|
||||
"service ServerToClient {" => {
|
||||
service = Some(RpcService::ServerToClient);
|
||||
continue;
|
||||
}
|
||||
"service ClientNotifications {" => {
|
||||
service = Some(RpcService::ClientNotifications);
|
||||
continue;
|
||||
}
|
||||
"service ServerNotifications {" => {
|
||||
service = Some(RpcService::ServerNotifications);
|
||||
continue;
|
||||
}
|
||||
"}" if current.is_none() => {
|
||||
service = None;
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(descriptor) = current.as_mut() {
|
||||
if let Some(value) = option_value(line, "jsonrpc_method") {
|
||||
descriptor.jsonrpc_method = value;
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = option_value(line, "rust_variant") {
|
||||
descriptor.variant = value;
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = option_value(line, "experimental_reason") {
|
||||
descriptor.experimental_reason = Some(value);
|
||||
continue;
|
||||
}
|
||||
if line == "}" {
|
||||
if let Some(descriptor) = current.take() {
|
||||
descriptors.push(descriptor);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(service) = service else {
|
||||
continue;
|
||||
};
|
||||
let Some(rpc) = parse_rpc_header(service, line) else {
|
||||
continue;
|
||||
};
|
||||
current = Some(rpc);
|
||||
}
|
||||
|
||||
descriptors
|
||||
}
|
||||
|
||||
fn parse_rpc_header(service: RpcService, line: &str) -> Option<RpcDescriptor> {
|
||||
let rest = line.strip_prefix("rpc ")?;
|
||||
let (variant, rest) = rest.split_once('(')?;
|
||||
let (params_type, rest) = rest.split_once(") returns (")?;
|
||||
let (response_type, _) = rest.split_once(')')?;
|
||||
Some(RpcDescriptor {
|
||||
service,
|
||||
variant: variant.to_string(),
|
||||
jsonrpc_method: String::new(),
|
||||
params_type: params_type.to_string(),
|
||||
response_type: response_type.to_string(),
|
||||
experimental_reason: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn option_value(line: &str, name: &str) -> Option<String> {
|
||||
let option_prefix = format!("option ({name}) = \"");
|
||||
let value = line.strip_prefix(&option_prefix)?;
|
||||
let value = value.strip_suffix("\";")?;
|
||||
Some(value.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parses_proto_registry_services() {
|
||||
assert_eq!(descriptors(RpcService::ClientToServer).count(), 98);
|
||||
assert_eq!(descriptors(RpcService::ServerToClient).count(), 9);
|
||||
assert_eq!(descriptors(RpcService::ClientNotifications).count(), 1);
|
||||
assert_eq!(descriptors(RpcService::ServerNotifications).count(), 62);
|
||||
|
||||
let thread_start =
|
||||
descriptor_by_method(RpcService::ClientToServer, "thread/start").unwrap();
|
||||
assert_eq!(thread_start.variant, "ThreadStart");
|
||||
assert_eq!(thread_start.params_type, "ThreadStartParams");
|
||||
assert_eq!(thread_start.response_type, "ThreadStartResponse");
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,12 @@ use std::path::PathBuf;
|
||||
|
||||
use crate::JSONRPCNotification;
|
||||
use crate::JSONRPCRequest;
|
||||
use crate::JSONRPCResponse;
|
||||
use crate::RequestId;
|
||||
use crate::export::GeneratedSchema;
|
||||
use crate::export::write_json_schema;
|
||||
use crate::proto::domain as v2;
|
||||
use crate::protocol::v1;
|
||||
use crate::protocol::v2;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -185,7 +186,20 @@ macro_rules! client_request_definitions {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn variant_name(&self) -> &'static str {
|
||||
match self {
|
||||
$(Self::$variant { .. } => stringify!($variant),)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method(&self) -> String {
|
||||
if let Some(descriptor) = crate::proto_registry::descriptor_by_variant(
|
||||
crate::proto_registry::RpcService::ClientToServer,
|
||||
self.variant_name(),
|
||||
) {
|
||||
return descriptor.jsonrpc_method.clone();
|
||||
}
|
||||
|
||||
serde_json::to_value(self)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
@@ -197,6 +211,23 @@ macro_rules! client_request_definitions {
|
||||
.unwrap_or_else(|| "<unknown>".to_string())
|
||||
}
|
||||
|
||||
pub fn from_jsonrpc_request(
|
||||
request: JSONRPCRequest,
|
||||
) -> std::result::Result<Self, serde_json::Error> {
|
||||
let _descriptor = crate::proto_registry::descriptor_by_method(
|
||||
crate::proto_registry::RpcService::ClientToServer,
|
||||
request.method.as_str(),
|
||||
);
|
||||
serde_json::from_value(serde_json::to_value(request)?)
|
||||
}
|
||||
|
||||
pub fn into_jsonrpc_request(self) -> std::result::Result<JSONRPCRequest, serde_json::Error> {
|
||||
let method = self.method();
|
||||
let mut request = serde_json::from_value::<JSONRPCRequest>(serde_json::to_value(self)?)?;
|
||||
request.method = method;
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
pub fn serialization_scope(&self) -> Option<ClientRequestSerializationScope> {
|
||||
match self {
|
||||
$(
|
||||
@@ -233,7 +264,20 @@ macro_rules! client_request_definitions {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn variant_name(&self) -> &'static str {
|
||||
match self {
|
||||
$(Self::$variant { .. } => stringify!($variant),)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method(&self) -> String {
|
||||
if let Some(descriptor) = crate::proto_registry::descriptor_by_variant(
|
||||
crate::proto_registry::RpcService::ClientToServer,
|
||||
self.variant_name(),
|
||||
) {
|
||||
return descriptor.jsonrpc_method.clone();
|
||||
}
|
||||
|
||||
serde_json::to_value(self)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
@@ -256,6 +300,13 @@ macro_rules! client_request_definitions {
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_jsonrpc_response(
|
||||
self,
|
||||
) -> std::result::Result<JSONRPCResponse, serde_json::Error> {
|
||||
let (id, result) = self.into_jsonrpc_parts()?;
|
||||
Ok(JSONRPCResponse { id, result })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -1022,6 +1073,31 @@ macro_rules! server_request_definitions {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn variant_name(&self) -> &'static str {
|
||||
match self {
|
||||
$(Self::$variant { .. } => stringify!($variant),)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method(&self) -> String {
|
||||
if let Some(descriptor) = crate::proto_registry::descriptor_by_variant(
|
||||
crate::proto_registry::RpcService::ServerToClient,
|
||||
self.variant_name(),
|
||||
) {
|
||||
return descriptor.jsonrpc_method.clone();
|
||||
}
|
||||
|
||||
serde_json::to_value(self)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("method")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_owned)
|
||||
})
|
||||
.unwrap_or_else(|| "<unknown>".to_string())
|
||||
}
|
||||
|
||||
pub fn response_from_result(
|
||||
&self,
|
||||
result: crate::Result,
|
||||
@@ -1038,6 +1114,13 @@ macro_rules! server_request_definitions {
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_jsonrpc_request(self) -> std::result::Result<JSONRPCRequest, serde_json::Error> {
|
||||
let method = self.method();
|
||||
let mut request = serde_json::from_value::<JSONRPCRequest>(serde_json::to_value(self)?)?;
|
||||
request.method = method;
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed response from the client to the server.
|
||||
@@ -1062,7 +1145,20 @@ macro_rules! server_request_definitions {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn variant_name(&self) -> &'static str {
|
||||
match self {
|
||||
$(Self::$variant { .. } => stringify!($variant),)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method(&self) -> String {
|
||||
if let Some(descriptor) = crate::proto_registry::descriptor_by_variant(
|
||||
crate::proto_registry::RpcService::ServerToClient,
|
||||
self.variant_name(),
|
||||
) {
|
||||
return descriptor.jsonrpc_method.clone();
|
||||
}
|
||||
|
||||
serde_json::to_value(self)
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
@@ -1073,6 +1169,21 @@ macro_rules! server_request_definitions {
|
||||
})
|
||||
.unwrap_or_else(|| "<unknown>".to_string())
|
||||
}
|
||||
|
||||
pub fn into_jsonrpc_response(
|
||||
self,
|
||||
) -> std::result::Result<JSONRPCResponse, serde_json::Error> {
|
||||
match self {
|
||||
$(
|
||||
Self::$variant { request_id, response } => {
|
||||
serde_json::to_value(response).map(|result| JSONRPCResponse {
|
||||
id: request_id,
|
||||
result,
|
||||
})
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, JsonSchema)]
|
||||
@@ -1166,11 +1277,39 @@ macro_rules! server_notification_definitions {
|
||||
}
|
||||
|
||||
impl ServerNotification {
|
||||
pub fn variant_name(&self) -> &'static str {
|
||||
match self {
|
||||
$(Self::$variant(_) => stringify!($variant),)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method(&self) -> String {
|
||||
if let Some(descriptor) = crate::proto_registry::descriptor_by_variant(
|
||||
crate::proto_registry::RpcService::ServerNotifications,
|
||||
self.variant_name(),
|
||||
) {
|
||||
return descriptor.jsonrpc_method.clone();
|
||||
}
|
||||
|
||||
self.to_string()
|
||||
}
|
||||
|
||||
pub fn to_params(self) -> Result<serde_json::Value, serde_json::Error> {
|
||||
match self {
|
||||
$(Self::$variant(params) => serde_json::to_value(params),)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_jsonrpc_notification(
|
||||
self,
|
||||
) -> Result<JSONRPCNotification, serde_json::Error> {
|
||||
let method = self.method();
|
||||
let params = self.to_params()?;
|
||||
Ok(JSONRPCNotification {
|
||||
method,
|
||||
params: Some(params),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JSONRPCNotification> for ServerNotification {
|
||||
@@ -1223,6 +1362,10 @@ impl TryFrom<JSONRPCRequest> for ServerRequest {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(value: JSONRPCRequest) -> Result<Self, Self::Error> {
|
||||
let _descriptor = crate::proto_registry::descriptor_by_method(
|
||||
crate::proto_registry::RpcService::ServerToClient,
|
||||
value.method.as_str(),
|
||||
);
|
||||
serde_json::from_value(serde_json::to_value(value)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,3 +42,144 @@ fn interrupt_conversation_payload_stays_jsonrpc_only() -> Result<()> {
|
||||
assert!(payload.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_request_method_comes_from_proto_registry() {
|
||||
let request = ClientRequest::ThreadArchive {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: v2::ThreadArchiveParams {
|
||||
thread_id: "thread-1".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(request.variant_name(), "ThreadArchive");
|
||||
assert_eq!(request.method(), "thread/archive");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_request_jsonrpc_shim_preserves_legacy_wire_shape() -> Result<()> {
|
||||
let request = JSONRPCRequest {
|
||||
id: RequestId::Integer(1),
|
||||
method: "thread/archive".to_string(),
|
||||
params: Some(json!({ "threadId": "thread-1" })),
|
||||
trace: None,
|
||||
};
|
||||
|
||||
let parsed = ClientRequest::from_jsonrpc_request(request)?;
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ClientRequest::ThreadArchive {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: v2::ThreadArchiveParams {
|
||||
thread_id: "thread-1".to_string(),
|
||||
},
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_request_into_jsonrpc_request_uses_proto_method_registry() -> Result<()> {
|
||||
let request = ClientRequest::ThreadArchive {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: v2::ThreadArchiveParams {
|
||||
thread_id: "thread-1".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let jsonrpc = request.into_jsonrpc_request()?;
|
||||
|
||||
assert_eq!(
|
||||
jsonrpc,
|
||||
JSONRPCRequest {
|
||||
id: RequestId::Integer(1),
|
||||
method: "thread/archive".to_string(),
|
||||
params: Some(json!({ "threadId": "thread-1" })),
|
||||
trace: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_response_into_jsonrpc_response_preserves_result_shape() -> Result<()> {
|
||||
let response = ClientResponse::ThreadArchive {
|
||||
request_id: RequestId::Integer(1),
|
||||
response: v2::ThreadArchiveResponse {},
|
||||
};
|
||||
|
||||
let jsonrpc = response.into_jsonrpc_response()?;
|
||||
|
||||
assert_eq!(
|
||||
jsonrpc,
|
||||
JSONRPCResponse {
|
||||
id: RequestId::Integer(1),
|
||||
result: json!({}),
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_request_and_response_jsonrpc_shims_preserve_wire_shape() -> Result<()> {
|
||||
let request = ServerRequest::ChatgptAuthTokensRefresh {
|
||||
request_id: RequestId::String("server-request-1".to_string()),
|
||||
params: v2::ChatgptAuthTokensRefreshParams {
|
||||
reason: v2::ChatgptAuthTokensRefreshReason::Unauthorized,
|
||||
previous_account_id: None,
|
||||
},
|
||||
};
|
||||
|
||||
let jsonrpc_request = request.into_jsonrpc_request()?;
|
||||
|
||||
assert_eq!(
|
||||
jsonrpc_request,
|
||||
JSONRPCRequest {
|
||||
id: RequestId::String("server-request-1".to_string()),
|
||||
method: "account/chatgptAuthTokens/refresh".to_string(),
|
||||
params: Some(json!({
|
||||
"reason": "unauthorized",
|
||||
"previousAccountId": null,
|
||||
})),
|
||||
trace: None,
|
||||
}
|
||||
);
|
||||
|
||||
let response = ServerResponse::ChatgptAuthTokensRefresh {
|
||||
request_id: RequestId::String("server-request-1".to_string()),
|
||||
response: v2::ChatgptAuthTokensRefreshResponse {
|
||||
access_token: "access-token".to_string(),
|
||||
chatgpt_account_id: "account-1".to_string(),
|
||||
chatgpt_plan_type: None,
|
||||
},
|
||||
};
|
||||
|
||||
let jsonrpc_response = response.into_jsonrpc_response()?;
|
||||
|
||||
assert_eq!(
|
||||
jsonrpc_response,
|
||||
JSONRPCResponse {
|
||||
id: RequestId::String("server-request-1".to_string()),
|
||||
result: json!({
|
||||
"accessToken": "access-token",
|
||||
"chatgptAccountId": "account-1",
|
||||
"chatgptPlanType": null,
|
||||
}),
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_notification_jsonrpc_shim_uses_proto_method_registry() -> Result<()> {
|
||||
let notification = ServerNotification::ThreadClosed(v2::ThreadClosedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
});
|
||||
|
||||
let jsonrpc = notification.into_jsonrpc_notification()?;
|
||||
|
||||
assert_eq!(jsonrpc.method, "thread/closed");
|
||||
assert_eq!(jsonrpc.params, Some(json!({ "threadId": "thread-1" })));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
use crate::proto::domain::AgentMessageDeltaNotification;
|
||||
use crate::proto::domain::CollabAgentState;
|
||||
use crate::proto::domain::CollabAgentTool;
|
||||
use crate::proto::domain::CollabAgentToolCallStatus;
|
||||
use crate::proto::domain::CommandExecutionOutputDeltaNotification;
|
||||
use crate::proto::domain::DynamicToolCallOutputContentItem;
|
||||
use crate::proto::domain::DynamicToolCallStatus;
|
||||
use crate::proto::domain::FileChangePatchUpdatedNotification;
|
||||
use crate::proto::domain::ItemCompletedNotification;
|
||||
use crate::proto::domain::ItemStartedNotification;
|
||||
use crate::proto::domain::McpToolCallError;
|
||||
use crate::proto::domain::McpToolCallResult;
|
||||
use crate::proto::domain::McpToolCallStatus;
|
||||
use crate::proto::domain::PlanDeltaNotification;
|
||||
use crate::proto::domain::ReasoningSummaryPartAddedNotification;
|
||||
use crate::proto::domain::ReasoningSummaryTextDeltaNotification;
|
||||
use crate::proto::domain::ReasoningTextDeltaNotification;
|
||||
use crate::proto::domain::TerminalInteractionNotification;
|
||||
use crate::proto::domain::ThreadItem;
|
||||
use crate::protocol::common::ServerNotification;
|
||||
use crate::protocol::item_builders::build_command_execution_begin_item;
|
||||
use crate::protocol::item_builders::build_command_execution_end_item;
|
||||
use crate::protocol::item_builders::convert_patch_changes;
|
||||
use crate::protocol::v2::AgentMessageDeltaNotification;
|
||||
use crate::protocol::v2::CollabAgentState;
|
||||
use crate::protocol::v2::CollabAgentTool;
|
||||
use crate::protocol::v2::CollabAgentToolCallStatus;
|
||||
use crate::protocol::v2::CommandExecutionOutputDeltaNotification;
|
||||
use crate::protocol::v2::DynamicToolCallOutputContentItem;
|
||||
use crate::protocol::v2::DynamicToolCallStatus;
|
||||
use crate::protocol::v2::FileChangePatchUpdatedNotification;
|
||||
use crate::protocol::v2::ItemCompletedNotification;
|
||||
use crate::protocol::v2::ItemStartedNotification;
|
||||
use crate::protocol::v2::McpToolCallError;
|
||||
use crate::protocol::v2::McpToolCallResult;
|
||||
use crate::protocol::v2::McpToolCallStatus;
|
||||
use crate::protocol::v2::PlanDeltaNotification;
|
||||
use crate::protocol::v2::ReasoningSummaryPartAddedNotification;
|
||||
use crate::protocol::v2::ReasoningSummaryTextDeltaNotification;
|
||||
use crate::protocol::v2::ReasoningTextDeltaNotification;
|
||||
use crate::protocol::v2::TerminalInteractionNotification;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
@@ -9,19 +9,19 @@
|
||||
//! synthetic items, so sharing the logic avoids drift between those paths.
|
||||
//! - The projection is presentation-specific. Core protocol events stay generic, while the
|
||||
//! app-server protocol decides how to surface those events as `ThreadItem`s for clients.
|
||||
use crate::proto::domain::AutoReviewDecisionSource;
|
||||
use crate::proto::domain::CommandAction;
|
||||
use crate::proto::domain::CommandExecutionSource;
|
||||
use crate::proto::domain::CommandExecutionStatus;
|
||||
use crate::proto::domain::FileUpdateChange;
|
||||
use crate::proto::domain::GuardianApprovalReview;
|
||||
use crate::proto::domain::GuardianApprovalReviewStatus;
|
||||
use crate::proto::domain::ItemGuardianApprovalReviewCompletedNotification;
|
||||
use crate::proto::domain::ItemGuardianApprovalReviewStartedNotification;
|
||||
use crate::proto::domain::PatchApplyStatus;
|
||||
use crate::proto::domain::PatchChangeKind;
|
||||
use crate::proto::domain::ThreadItem;
|
||||
use crate::protocol::common::ServerNotification;
|
||||
use crate::protocol::v2::AutoReviewDecisionSource;
|
||||
use crate::protocol::v2::CommandAction;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use crate::protocol::v2::CommandExecutionStatus;
|
||||
use crate::protocol::v2::FileUpdateChange;
|
||||
use crate::protocol::v2::GuardianApprovalReview;
|
||||
use crate::protocol::v2::GuardianApprovalReviewStatus;
|
||||
use crate::protocol::v2::ItemGuardianApprovalReviewCompletedNotification;
|
||||
use crate::protocol::v2::ItemGuardianApprovalReviewStartedNotification;
|
||||
use crate::protocol::v2::PatchApplyStatus;
|
||||
use crate::protocol::v2::PatchChangeKind;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::proto::domain as v2;
|
||||
use crate::protocol::v1;
|
||||
use crate::protocol::v2;
|
||||
impl From<v1::ExecOneOffCommandParams> for v2::CommandExecParams {
|
||||
fn from(value: v1::ExecOneOffCommandParams) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -5,7 +5,6 @@ pub mod common;
|
||||
pub mod event_mapping;
|
||||
pub mod item_builders;
|
||||
mod mappers;
|
||||
mod serde_helpers;
|
||||
pub(crate) mod serde_helpers;
|
||||
pub mod thread_history;
|
||||
pub mod v1;
|
||||
pub mod v2;
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
use crate::proto::domain::CollabAgentState;
|
||||
use crate::proto::domain::CollabAgentTool;
|
||||
use crate::proto::domain::CollabAgentToolCallStatus;
|
||||
use crate::proto::domain::CommandExecutionStatus;
|
||||
use crate::proto::domain::DynamicToolCallOutputContentItem;
|
||||
use crate::proto::domain::DynamicToolCallStatus;
|
||||
use crate::proto::domain::McpToolCallError;
|
||||
use crate::proto::domain::McpToolCallResult;
|
||||
use crate::proto::domain::McpToolCallStatus;
|
||||
use crate::proto::domain::ThreadItem;
|
||||
use crate::proto::domain::Turn;
|
||||
use crate::proto::domain::TurnError as V2TurnError;
|
||||
use crate::proto::domain::TurnError;
|
||||
use crate::proto::domain::TurnStatus;
|
||||
use crate::proto::domain::UserInput;
|
||||
use crate::proto::domain::WebSearchAction;
|
||||
use crate::protocol::item_builders::build_command_execution_begin_item;
|
||||
use crate::protocol::item_builders::build_command_execution_end_item;
|
||||
use crate::protocol::item_builders::build_file_change_approval_request_item;
|
||||
use crate::protocol::item_builders::build_file_change_begin_item;
|
||||
use crate::protocol::item_builders::build_file_change_end_item;
|
||||
use crate::protocol::item_builders::build_item_from_guardian_event;
|
||||
use crate::protocol::v2::CollabAgentState;
|
||||
use crate::protocol::v2::CollabAgentTool;
|
||||
use crate::protocol::v2::CollabAgentToolCallStatus;
|
||||
use crate::protocol::v2::CommandExecutionStatus;
|
||||
use crate::protocol::v2::DynamicToolCallOutputContentItem;
|
||||
use crate::protocol::v2::DynamicToolCallStatus;
|
||||
use crate::protocol::v2::McpToolCallError;
|
||||
use crate::protocol::v2::McpToolCallResult;
|
||||
use crate::protocol::v2::McpToolCallStatus;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use crate::protocol::v2::Turn;
|
||||
use crate::protocol::v2::TurnError as V2TurnError;
|
||||
use crate::protocol::v2::TurnError;
|
||||
use crate::protocol::v2::TurnStatus;
|
||||
use crate::protocol::v2::UserInput;
|
||||
use crate::protocol::v2::WebSearchAction;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
@@ -58,13 +58,13 @@ use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::protocol::v2::CommandAction;
|
||||
use crate::proto::domain::CommandAction;
|
||||
#[cfg(test)]
|
||||
use crate::protocol::v2::FileUpdateChange;
|
||||
use crate::proto::domain::FileUpdateChange;
|
||||
#[cfg(test)]
|
||||
use crate::protocol::v2::PatchApplyStatus;
|
||||
use crate::proto::domain::PatchApplyStatus;
|
||||
#[cfg(test)]
|
||||
use crate::protocol::v2::PatchChangeKind;
|
||||
use crate::proto::domain::PatchChangeKind;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus;
|
||||
#[cfg(test)]
|
||||
@@ -256,7 +256,7 @@ impl ThreadHistoryBuilder {
|
||||
fragments: hook_prompt
|
||||
.fragments
|
||||
.into_iter()
|
||||
.map(crate::protocol::v2::HookPromptFragment::from)
|
||||
.map(crate::proto::domain::HookPromptFragment::from)
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
@@ -285,7 +285,7 @@ impl ThreadHistoryBuilder {
|
||||
&mut self,
|
||||
text: String,
|
||||
phase: Option<MessagePhase>,
|
||||
memory_citation: Option<crate::protocol::v2::MemoryCitation>,
|
||||
memory_citation: Option<crate::proto::domain::MemoryCitation>,
|
||||
) {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
@@ -1190,7 +1190,7 @@ impl From<&PendingTurn> for Turn {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use crate::proto::domain::CommandExecutionSource;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment;
|
||||
@@ -2761,7 +2761,7 @@ mod tests {
|
||||
agents_states: [(
|
||||
"00000000-0000-0000-0000-000000000002".into(),
|
||||
CollabAgentState {
|
||||
status: crate::protocol::v2::CollabAgentStatus::Completed,
|
||||
status: crate::proto::domain::CollabAgentStatus::Completed,
|
||||
message: None,
|
||||
},
|
||||
)]
|
||||
@@ -2818,7 +2818,7 @@ mod tests {
|
||||
agents_states: [(
|
||||
"00000000-0000-0000-0000-000000000002".into(),
|
||||
CollabAgentState {
|
||||
status: crate::protocol::v2::CollabAgentStatus::Running,
|
||||
status: crate::proto::domain::CollabAgentStatus::Running,
|
||||
message: None,
|
||||
},
|
||||
)]
|
||||
@@ -2886,7 +2886,7 @@ mod tests {
|
||||
agents_states: [(
|
||||
receiver.to_string(),
|
||||
CollabAgentState {
|
||||
status: crate::protocol::v2::CollabAgentStatus::Interrupted,
|
||||
status: crate::proto::domain::CollabAgentStatus::Interrupted,
|
||||
message: None,
|
||||
},
|
||||
)]
|
||||
@@ -3023,7 +3023,7 @@ mod tests {
|
||||
Some(TurnError {
|
||||
message: "stream failure".into(),
|
||||
codex_error_info: Some(
|
||||
crate::protocol::v2::CodexErrorInfo::ResponseStreamDisconnected {
|
||||
crate::proto::domain::CodexErrorInfo::ResponseStreamDisconnected {
|
||||
http_status_code: Some(502),
|
||||
}
|
||||
),
|
||||
@@ -3071,11 +3071,11 @@ mod tests {
|
||||
ThreadItem::HookPrompt {
|
||||
id: turns[0].items[1].id().to_string(),
|
||||
fragments: vec![
|
||||
crate::protocol::v2::HookPromptFragment {
|
||||
crate::proto::domain::HookPromptFragment {
|
||||
text: "Retry with tests.".into(),
|
||||
hook_run_id: "hook-run-1".into(),
|
||||
},
|
||||
crate::protocol::v2::HookPromptFragment {
|
||||
crate::proto::domain::HookPromptFragment {
|
||||
text: "Then summarize cleanly.".into(),
|
||||
hook_run_id: "hook-run-2".into(),
|
||||
},
|
||||
|
||||
204
codex-rs/app-server-protocol/tests/proto_compat.rs
Normal file
204
codex-rs/app-server-protocol/tests/proto_compat.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use anyhow::Result;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshParams;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshReason;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse;
|
||||
use codex_app_server_protocol::ClientNotification;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponse;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ServerResponse;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadArchiveResponse;
|
||||
use codex_app_server_protocol::ThreadClosedNotification;
|
||||
use codex_app_server_protocol::proto::jsonrpc;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json as json_value;
|
||||
|
||||
fn round_trip_jsonrpc_message(message: JSONRPCMessage) -> Result<JSONRPCMessage> {
|
||||
let encoded = jsonrpc::encode_jsonrpc_message(message);
|
||||
jsonrpc::decode_jsonrpc_message(&encoded)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_request_round_trips_through_protobuf_wire() -> Result<()> {
|
||||
let original = ClientRequest::ThreadArchive {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: ThreadArchiveParams {
|
||||
thread_id: "thread-1".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let jsonrpc = original
|
||||
.clone()
|
||||
.into_jsonrpc_request()
|
||||
.expect("convert to JSON-RPC request");
|
||||
let round_tripped_jsonrpc = match round_trip_jsonrpc_message(JSONRPCMessage::Request(jsonrpc))?
|
||||
{
|
||||
JSONRPCMessage::Request(request) => request,
|
||||
_ => panic!("expected request"),
|
||||
};
|
||||
let round_tripped =
|
||||
ClientRequest::from_jsonrpc_request(round_tripped_jsonrpc).expect("parse client request");
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(round_tripped).expect("serialize round-tripped request"),
|
||||
serde_json::to_value(original).expect("serialize original request")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_response_round_trips_through_protobuf_wire() -> Result<()> {
|
||||
let original = ClientResponse::ThreadArchive {
|
||||
request_id: RequestId::Integer(1),
|
||||
response: ThreadArchiveResponse {},
|
||||
};
|
||||
let jsonrpc = original
|
||||
.into_jsonrpc_response()
|
||||
.expect("convert to JSON-RPC response");
|
||||
|
||||
let round_tripped = match round_trip_jsonrpc_message(JSONRPCMessage::Response(jsonrpc.clone()))?
|
||||
{
|
||||
JSONRPCMessage::Response(response) => response,
|
||||
_ => panic!("expected response"),
|
||||
};
|
||||
|
||||
assert_eq!(round_tripped, jsonrpc);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_request_round_trips_through_protobuf_wire() -> Result<()> {
|
||||
let original = ServerRequest::ChatgptAuthTokensRefresh {
|
||||
request_id: RequestId::String("server-request-1".to_string()),
|
||||
params: ChatgptAuthTokensRefreshParams {
|
||||
reason: ChatgptAuthTokensRefreshReason::Unauthorized,
|
||||
previous_account_id: None,
|
||||
},
|
||||
};
|
||||
|
||||
let jsonrpc = original
|
||||
.clone()
|
||||
.into_jsonrpc_request()
|
||||
.expect("convert to JSON-RPC request");
|
||||
let round_tripped_jsonrpc = match round_trip_jsonrpc_message(JSONRPCMessage::Request(jsonrpc))?
|
||||
{
|
||||
JSONRPCMessage::Request(request) => request,
|
||||
_ => panic!("expected request"),
|
||||
};
|
||||
let round_tripped =
|
||||
ServerRequest::try_from(round_tripped_jsonrpc).expect("parse server request");
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(round_tripped).expect("serialize round-tripped request"),
|
||||
serde_json::to_value(original).expect("serialize original request")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_response_round_trips_through_protobuf_wire() -> Result<()> {
|
||||
let original = ServerResponse::ChatgptAuthTokensRefresh {
|
||||
request_id: RequestId::String("server-request-1".to_string()),
|
||||
response: ChatgptAuthTokensRefreshResponse {
|
||||
access_token: "access-token".to_string(),
|
||||
chatgpt_account_id: "account-1".to_string(),
|
||||
chatgpt_plan_type: None,
|
||||
},
|
||||
};
|
||||
let jsonrpc = original
|
||||
.into_jsonrpc_response()
|
||||
.expect("convert to JSON-RPC response");
|
||||
|
||||
let round_tripped = match round_trip_jsonrpc_message(JSONRPCMessage::Response(jsonrpc.clone()))?
|
||||
{
|
||||
JSONRPCMessage::Response(response) => response,
|
||||
_ => panic!("expected response"),
|
||||
};
|
||||
|
||||
assert_eq!(round_tripped, jsonrpc);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_notification_round_trips_through_protobuf_wire() -> Result<()> {
|
||||
let original = ServerNotification::ThreadClosed(ThreadClosedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
});
|
||||
|
||||
let jsonrpc = original
|
||||
.clone()
|
||||
.into_jsonrpc_notification()
|
||||
.expect("convert to JSON-RPC notification");
|
||||
let round_tripped_jsonrpc =
|
||||
match round_trip_jsonrpc_message(JSONRPCMessage::Notification(jsonrpc))? {
|
||||
JSONRPCMessage::Notification(notification) => notification,
|
||||
_ => panic!("expected notification"),
|
||||
};
|
||||
let round_tripped =
|
||||
ServerNotification::try_from(round_tripped_jsonrpc).expect("parse server notification");
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(round_tripped).expect("serialize round-tripped notification"),
|
||||
serde_json::to_value(original).expect("serialize original notification")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_notification_round_trips_through_protobuf_wire() -> Result<()> {
|
||||
let original = ClientNotification::Initialized;
|
||||
let jsonrpc = serde_json::from_value::<JSONRPCNotification>(
|
||||
serde_json::to_value(&original).expect("serialize client notification"),
|
||||
)
|
||||
.expect("convert to JSON-RPC notification");
|
||||
|
||||
let round_tripped_jsonrpc =
|
||||
match round_trip_jsonrpc_message(JSONRPCMessage::Notification(jsonrpc))? {
|
||||
JSONRPCMessage::Notification(notification) => notification,
|
||||
_ => panic!("expected notification"),
|
||||
};
|
||||
let round_tripped = serde_json::from_value::<ClientNotification>(
|
||||
serde_json::to_value(round_tripped_jsonrpc).expect("serialize JSON-RPC notification"),
|
||||
)
|
||||
.expect("parse client notification");
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(round_tripped).expect("serialize round-tripped notification"),
|
||||
serde_json::to_value(original).expect("serialize original notification")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_error_round_trips_through_protobuf_wire() -> Result<()> {
|
||||
let original = JSONRPCError {
|
||||
id: RequestId::Integer(1),
|
||||
error: JSONRPCErrorError {
|
||||
code: -32601,
|
||||
message: "method not found".to_string(),
|
||||
data: Some(json_value!({ "method": "missing/method" })),
|
||||
},
|
||||
};
|
||||
|
||||
let round_tripped = match round_trip_jsonrpc_message(JSONRPCMessage::Error(original.clone()))? {
|
||||
JSONRPCMessage::Error(error) => error,
|
||||
_ => panic!("expected error"),
|
||||
};
|
||||
|
||||
assert_eq!(round_tripped, original);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,8 +4,11 @@ use crate::outgoing_message::ConnectionId;
|
||||
use crate::outgoing_message::OutgoingError;
|
||||
use crate::outgoing_message::OutgoingMessage;
|
||||
use crate::outgoing_message::QueuedOutgoingMessage;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::proto::jsonrpc;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::net::SocketAddr;
|
||||
@@ -202,6 +205,23 @@ async fn forward_incoming_message(
|
||||
}
|
||||
}
|
||||
|
||||
async fn forward_incoming_protobuf_message(
|
||||
transport_event_tx: &mpsc::Sender<TransportEvent>,
|
||||
writer: &mpsc::Sender<QueuedOutgoingMessage>,
|
||||
connection_id: ConnectionId,
|
||||
payload: &[u8],
|
||||
) -> bool {
|
||||
match jsonrpc::decode_jsonrpc_message(payload) {
|
||||
Ok(message) => {
|
||||
enqueue_incoming_message(transport_event_tx, writer, connection_id, message).await
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to deserialize protobuf JSONRPCMessage: {err}");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn enqueue_incoming_message(
|
||||
transport_event_tx: &mpsc::Sender<TransportEvent>,
|
||||
writer: &mpsc::Sender<QueuedOutgoingMessage>,
|
||||
@@ -260,6 +280,39 @@ fn serialize_outgoing_message(outgoing_message: OutgoingMessage) -> Option<Strin
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_outgoing_protobuf_message(outgoing_message: OutgoingMessage) -> Option<Vec<u8>> {
|
||||
outgoing_message_to_jsonrpc(outgoing_message).map(jsonrpc::encode_jsonrpc_message)
|
||||
}
|
||||
|
||||
fn outgoing_message_to_jsonrpc(outgoing_message: OutgoingMessage) -> Option<JSONRPCMessage> {
|
||||
match outgoing_message {
|
||||
OutgoingMessage::Request(request) => match request.into_jsonrpc_request() {
|
||||
Ok(request) => Some(JSONRPCMessage::Request(request)),
|
||||
Err(err) => {
|
||||
error!("Failed to convert ServerRequest to JSON-RPC request: {err}");
|
||||
None
|
||||
}
|
||||
},
|
||||
OutgoingMessage::AppServerNotification(notification) => {
|
||||
match notification.into_jsonrpc_notification() {
|
||||
Ok(notification) => Some(JSONRPCMessage::Notification(notification)),
|
||||
Err(err) => {
|
||||
error!("Failed to convert ServerNotification to JSON-RPC notification: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
OutgoingMessage::Response(response) => Some(JSONRPCMessage::Response(JSONRPCResponse {
|
||||
id: response.id,
|
||||
result: response.result,
|
||||
})),
|
||||
OutgoingMessage::Error(error) => Some(JSONRPCMessage::Error(JSONRPCError {
|
||||
error: error.error,
|
||||
id: error.id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -3,8 +3,13 @@ use super::CHANNEL_CAPACITY;
|
||||
use super::TransportEvent;
|
||||
use super::app_server_control_socket_path;
|
||||
use super::start_control_socket_acceptor;
|
||||
use crate::OutgoingMessage;
|
||||
use crate::QueuedOutgoingMessage;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::proto::jsonrpc;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_uds::UnixStream;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -79,8 +84,12 @@ async fn control_socket_acceptor_upgrades_and_forwards_websocket_text_messages_a
|
||||
.await
|
||||
.expect("connection opened event should arrive")
|
||||
.expect("connection opened event");
|
||||
let connection_id = match opened {
|
||||
TransportEvent::ConnectionOpened { connection_id, .. } => connection_id,
|
||||
let (connection_id, writer) = match opened {
|
||||
TransportEvent::ConnectionOpened {
|
||||
connection_id,
|
||||
writer,
|
||||
..
|
||||
} => (connection_id, writer),
|
||||
_ => panic!("expected connection opened event"),
|
||||
};
|
||||
|
||||
@@ -112,6 +121,58 @@ async fn control_socket_acceptor_upgrades_and_forwards_websocket_text_messages_a
|
||||
(connection_id, notification)
|
||||
);
|
||||
|
||||
let binary_notification = JSONRPCMessage::Notification(JSONRPCNotification {
|
||||
method: "binary/initialized".to_string(),
|
||||
params: None,
|
||||
});
|
||||
websocket
|
||||
.send(WebSocketMessage::Binary(
|
||||
jsonrpc::encode_jsonrpc_message(binary_notification.clone()).into(),
|
||||
))
|
||||
.await
|
||||
.expect("binary notification should send");
|
||||
|
||||
let incoming = timeout(Duration::from_secs(1), transport_event_rx.recv())
|
||||
.await
|
||||
.expect("binary incoming message event should arrive")
|
||||
.expect("binary incoming message event");
|
||||
assert_eq!(
|
||||
match incoming {
|
||||
TransportEvent::IncomingMessage {
|
||||
connection_id: incoming_connection_id,
|
||||
message,
|
||||
} => (incoming_connection_id, message),
|
||||
_ => panic!("expected incoming message event"),
|
||||
},
|
||||
(connection_id, binary_notification)
|
||||
);
|
||||
|
||||
writer
|
||||
.send(QueuedOutgoingMessage::new(
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::ConfigWarning(
|
||||
ConfigWarningNotification {
|
||||
summary: "binary mode".to_string(),
|
||||
details: None,
|
||||
path: None,
|
||||
range: None,
|
||||
},
|
||||
)),
|
||||
))
|
||||
.await
|
||||
.expect("outbound message should enqueue");
|
||||
let frame = timeout(Duration::from_secs(1), websocket.next())
|
||||
.await
|
||||
.expect("binary outbound frame should arrive")
|
||||
.expect("binary outbound frame")
|
||||
.expect("binary outbound frame should be valid");
|
||||
let WebSocketMessage::Binary(payload) = frame else {
|
||||
panic!("expected protobuf binary outbound frame");
|
||||
};
|
||||
assert!(matches!(
|
||||
jsonrpc::decode_jsonrpc_message(&payload).expect("binary frame should decode"),
|
||||
JSONRPCMessage::Notification(JSONRPCNotification { method, .. }) if method == "configWarning"
|
||||
));
|
||||
|
||||
websocket
|
||||
.send(WebSocketMessage::Ping(Bytes::from_static(b"check")))
|
||||
.await
|
||||
|
||||
@@ -4,7 +4,9 @@ use super::TransportEvent;
|
||||
use super::auth::WebsocketAuthPolicy;
|
||||
use super::auth::authorize_upgrade;
|
||||
use super::auth::should_warn_about_unauthenticated_non_loopback_listener;
|
||||
use super::encode_outgoing_protobuf_message;
|
||||
use super::forward_incoming_message;
|
||||
use super::forward_incoming_protobuf_message;
|
||||
use super::next_connection_id;
|
||||
use super::serialize_outgoing_message;
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
@@ -34,6 +36,8 @@ use owo_colors::Style;
|
||||
use std::io::Result as IoResult;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -183,6 +187,7 @@ pub(crate) async fn run_websocket_connection<M, SinkError, StreamError>(
|
||||
mpsc::channel::<QueuedOutgoingMessage>(WEBSOCKET_OUTBOUND_CHANNEL_CAPACITY);
|
||||
let writer_tx_for_reader = writer_tx.clone();
|
||||
let disconnect_token = CancellationToken::new();
|
||||
let protobuf_mode = Arc::new(AtomicBool::new(false));
|
||||
if transport_event_tx
|
||||
.send(TransportEvent::ConnectionOpened {
|
||||
connection_id,
|
||||
@@ -202,6 +207,7 @@ pub(crate) async fn run_websocket_connection<M, SinkError, StreamError>(
|
||||
writer_rx,
|
||||
writer_control_rx,
|
||||
disconnect_token.clone(),
|
||||
Arc::clone(&protobuf_mode),
|
||||
));
|
||||
let mut inbound_task = tokio::spawn(run_websocket_inbound_loop(
|
||||
websocket_reader,
|
||||
@@ -210,6 +216,7 @@ pub(crate) async fn run_websocket_connection<M, SinkError, StreamError>(
|
||||
writer_control_tx,
|
||||
connection_id,
|
||||
disconnect_token.clone(),
|
||||
protobuf_mode,
|
||||
));
|
||||
|
||||
tokio::select! {
|
||||
@@ -230,7 +237,7 @@ pub(crate) async fn run_websocket_connection<M, SinkError, StreamError>(
|
||||
|
||||
pub(crate) enum IncomingWebSocketMessage {
|
||||
Text(String),
|
||||
Binary,
|
||||
Binary(Bytes),
|
||||
Ping(Bytes),
|
||||
Pong,
|
||||
Close,
|
||||
@@ -241,6 +248,7 @@ pub(crate) enum IncomingWebSocketMessage {
|
||||
/// sends directly.
|
||||
pub(crate) trait AppServerWebSocketMessage: Sized {
|
||||
fn text(text: String) -> Self;
|
||||
fn binary(payload: Vec<u8>) -> Self;
|
||||
fn pong(payload: Bytes) -> Self;
|
||||
fn into_incoming(self) -> Option<IncomingWebSocketMessage>;
|
||||
}
|
||||
@@ -250,6 +258,10 @@ impl AppServerWebSocketMessage for AxumWebSocketMessage {
|
||||
Self::Text(text.into())
|
||||
}
|
||||
|
||||
fn binary(payload: Vec<u8>) -> Self {
|
||||
Self::Binary(payload.into())
|
||||
}
|
||||
|
||||
fn pong(payload: Bytes) -> Self {
|
||||
Self::Pong(payload)
|
||||
}
|
||||
@@ -257,7 +269,7 @@ impl AppServerWebSocketMessage for AxumWebSocketMessage {
|
||||
fn into_incoming(self) -> Option<IncomingWebSocketMessage> {
|
||||
Some(match self {
|
||||
Self::Text(text) => IncomingWebSocketMessage::Text(text.to_string()),
|
||||
Self::Binary(_) => IncomingWebSocketMessage::Binary,
|
||||
Self::Binary(payload) => IncomingWebSocketMessage::Binary(payload),
|
||||
Self::Ping(payload) => IncomingWebSocketMessage::Ping(payload),
|
||||
Self::Pong(_) => IncomingWebSocketMessage::Pong,
|
||||
Self::Close(_) => IncomingWebSocketMessage::Close,
|
||||
@@ -270,6 +282,10 @@ impl AppServerWebSocketMessage for TungsteniteWebSocketMessage {
|
||||
Self::Text(text.into())
|
||||
}
|
||||
|
||||
fn binary(payload: Vec<u8>) -> Self {
|
||||
Self::Binary(payload.into())
|
||||
}
|
||||
|
||||
fn pong(payload: Bytes) -> Self {
|
||||
Self::Pong(payload)
|
||||
}
|
||||
@@ -277,7 +293,7 @@ impl AppServerWebSocketMessage for TungsteniteWebSocketMessage {
|
||||
fn into_incoming(self) -> Option<IncomingWebSocketMessage> {
|
||||
Some(match self {
|
||||
Self::Text(text) => IncomingWebSocketMessage::Text(text.to_string()),
|
||||
Self::Binary(_) => IncomingWebSocketMessage::Binary,
|
||||
Self::Binary(payload) => IncomingWebSocketMessage::Binary(payload),
|
||||
Self::Ping(payload) => IncomingWebSocketMessage::Ping(payload),
|
||||
Self::Pong(_) => IncomingWebSocketMessage::Pong,
|
||||
Self::Close(_) => IncomingWebSocketMessage::Close,
|
||||
@@ -291,6 +307,7 @@ async fn run_websocket_outbound_loop<M, SinkError>(
|
||||
mut writer_rx: mpsc::Receiver<QueuedOutgoingMessage>,
|
||||
mut writer_control_rx: mpsc::Receiver<M>,
|
||||
disconnect_token: CancellationToken,
|
||||
protobuf_mode: Arc<AtomicBool>,
|
||||
) where
|
||||
M: AppServerWebSocketMessage + Send + 'static,
|
||||
SinkError: Send + 'static,
|
||||
@@ -313,10 +330,18 @@ async fn run_websocket_outbound_loop<M, SinkError>(
|
||||
let Some(queued_message) = queued_message else {
|
||||
break;
|
||||
};
|
||||
let Some(json) = serialize_outgoing_message(queued_message.message) else {
|
||||
continue;
|
||||
let message = if protobuf_mode.load(Ordering::Acquire) {
|
||||
let Some(payload) = encode_outgoing_protobuf_message(queued_message.message) else {
|
||||
continue;
|
||||
};
|
||||
M::binary(payload)
|
||||
} else {
|
||||
let Some(json) = serialize_outgoing_message(queued_message.message) else {
|
||||
continue;
|
||||
};
|
||||
M::text(json)
|
||||
};
|
||||
if websocket_writer.send(M::text(json)).await.is_err() {
|
||||
if websocket_writer.send(message).await.is_err() {
|
||||
break;
|
||||
}
|
||||
if let Some(write_complete_tx) = queued_message.write_complete_tx {
|
||||
@@ -334,6 +359,7 @@ async fn run_websocket_inbound_loop<M, StreamError>(
|
||||
writer_control_tx: mpsc::Sender<M>,
|
||||
connection_id: ConnectionId,
|
||||
disconnect_token: CancellationToken,
|
||||
protobuf_mode: Arc<AtomicBool>,
|
||||
) where
|
||||
M: AppServerWebSocketMessage + Send + 'static,
|
||||
StreamError: std::fmt::Display + Send + 'static,
|
||||
@@ -371,8 +397,18 @@ async fn run_websocket_inbound_loop<M, StreamError>(
|
||||
}
|
||||
Some(IncomingWebSocketMessage::Pong) => {}
|
||||
Some(IncomingWebSocketMessage::Close) => break,
|
||||
Some(IncomingWebSocketMessage::Binary) => {
|
||||
warn!("dropping unsupported binary websocket message");
|
||||
Some(IncomingWebSocketMessage::Binary(payload)) => {
|
||||
protobuf_mode.store(true, Ordering::Release);
|
||||
if !forward_incoming_protobuf_message(
|
||||
&transport_event_tx,
|
||||
&writer_tx_for_reader,
|
||||
connection_id,
|
||||
&payload,
|
||||
)
|
||||
.await
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
},
|
||||
|
||||
@@ -66,7 +66,9 @@ Backpressure behavior:
|
||||
|
||||
## Message Schema
|
||||
|
||||
Currently, you can dump a TypeScript version of the schema using `codex app-server generate-ts`, or a JSON Schema bundle via `codex app-server generate-json-schema`. Each output is specific to the version of Codex you used to run the command, so the generated artifacts are guaranteed to match that version.
|
||||
The app-server protocol registry is defined in protobuf at `app-server-protocol/proto/codex.app_server.v2.proto`. JSON-RPC remains the compatibility wire format for current transports, and the Rust facade, TypeScript bindings, and JSON Schema artifacts are generated or checked against that protobuf registry so method names stay aligned.
|
||||
|
||||
You can dump a TypeScript version of the JSON-RPC-facing schema using `codex app-server generate-ts`, or a JSON Schema bundle via `codex app-server generate-json-schema`. Each output is specific to the version of Codex you used to run the command, so the generated artifacts are guaranteed to match that version.
|
||||
|
||||
```
|
||||
codex app-server generate-ts --out DIR
|
||||
@@ -1807,7 +1809,7 @@ Use this checklist when introducing a field/method that should only be available
|
||||
|
||||
At runtime, clients must send `initialize` with `capabilities.experimentalApi = true` to use experimental methods or fields.
|
||||
|
||||
1. Annotate the field in the protocol type (usually `app-server-protocol/src/protocol/v2.rs`) with:
|
||||
1. Annotate the field in the protocol domain type (usually `app-server-protocol/src/proto/domain.rs`) with:
|
||||
```rust
|
||||
#[experimental("thread/start.myField")]
|
||||
pub my_field: Option<String>,
|
||||
|
||||
@@ -395,9 +395,7 @@ impl MessageProcessor {
|
||||
request_context.clone(),
|
||||
async {
|
||||
let result = async {
|
||||
let request_json = serde_json::to_value(&request)
|
||||
.map_err(|err| invalid_request(format!("Invalid request: {err}")))?;
|
||||
let codex_request = serde_json::from_value::<ClientRequest>(request_json)
|
||||
let codex_request = ClientRequest::from_jsonrpc_request(request.clone())
|
||||
.map_err(|err| invalid_request(format!("Invalid request: {err}")))?;
|
||||
// Websocket callers finalize outbound readiness in lib.rs after mirroring
|
||||
// session state into outbound state and sending initialize notifications to
|
||||
|
||||
Reference in New Issue
Block a user