netget/client/msgpack_rpc/
actions.rs1use crate::llm::actions::{
2 client_trait::{Client, ClientActionResult},
3 protocol_trait::Protocol,
4 ActionDefinition,
5};
6use crate::protocol::{ConnectContext, EventType};
7use crate::server::msgpack_rpc::actions::{
8 action, check_notify, check_value, notify_action, parameter, VALUE_NOTE,
9};
10use crate::state::app_state::AppState;
11use anyhow::{bail, ensure, Result};
12use serde_json::{json, Value};
13use std::sync::LazyLock;
14
15#[derive(Default)]
16pub struct MsgpackRpcClientProtocol;
17impl MsgpackRpcClientProtocol {
18 pub fn new() -> Self {
19 Self
20 }
21}
22
23fn call_action() -> ActionDefinition {
24 action(
25 "msgpack_call",
26 "Call a method on the server; the result or error arrives as msgpack_response.",
27 vec![
28 parameter("method", "string", "Method name, e.g. nvim_eval", true),
29 parameter("params", "array", VALUE_NOTE, true),
30 ],
31 json!({"type":"msgpack_call","method":"nvim_eval","params":["1 + 2"]}),
32 )
33}
34
35fn disconnect_action() -> ActionDefinition {
36 action(
37 "disconnect",
38 "Close the MessagePack-RPC connection",
39 vec![],
40 json!({"type":"disconnect"}),
41 )
42}
43
44fn actions() -> Vec<ActionDefinition> {
45 vec![call_action(), notify_action(), disconnect_action()]
46}
47
48pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
49 EventType::new(
50 "msgpack_connected",
51 "Connected to the MessagePack-RPC server.",
52 call_action().example.clone(),
53 )
54 .with_parameters(vec![parameter(
55 "remote_addr",
56 "string",
57 "Server address and port",
58 true,
59 )])
60 .with_actions(actions())
61});
62
63pub static RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
64 EventType::new(
65 "msgpack_response",
66 "The server answered a call.",
67 call_action().example.clone(),
68 )
69 .with_parameters(vec![
70 parameter("method", "string", "The method that was called", true),
71 parameter("msgid", "number", "The call's message id", true),
72 parameter("result", "any", "The result, null on error", true),
73 parameter("error", "any", "The error value, null on success", true),
74 ])
75 .with_actions(actions())
76});
77
78pub static NOTIFICATION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
79 EventType::new(
80 "msgpack_notification",
81 "The server sent a notification.",
82 call_action().example.clone(),
83 )
84 .with_parameters(vec![
85 parameter("method", "string", "Notification method name", true),
86 parameter("params", "array", "The parameters, as an array", true),
87 ])
88 .with_actions(actions())
89});
90
91pub fn check(v: &Value) -> Result<()> {
92 match v["type"].as_str() {
93 Some("msgpack_call") => {
94 ensure!(
95 v["method"].as_str().is_some_and(|m| !m.is_empty()),
96 "method must be a non-empty string"
97 );
98 ensure!(v["params"].is_array(), "params must be an array");
99 check_value(&v["params"])
100 }
101 Some("msgpack_notify") => check_notify(v),
102 _ => bail!("Unknown MessagePack-RPC client action"),
103 }
104}
105
106impl Protocol for MsgpackRpcClientProtocol {
107 fn protocol_name(&self) -> &'static str {
108 "MessagePack-RPC"
109 }
110 fn stack_name(&self) -> &'static str {
111 "ETH>IP>TCP>MessagePack-RPC"
112 }
113 fn keywords(&self) -> Vec<&'static str> {
114 vec!["msgpack-rpc", "messagepack-rpc", "msgpack", "neovim rpc"]
115 }
116 fn description(&self) -> &'static str {
117 "MessagePack-RPC client: calls, notifications, and the server's notifications as events"
118 }
119 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
120 actions()
121 }
122 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
123 vec![]
124 }
125 fn get_event_types(&self) -> Vec<EventType> {
126 vec![
127 CONNECTED_EVENT.clone(),
128 RESPONSE_EVENT.clone(),
129 NOTIFICATION_EVENT.clone(),
130 ]
131 }
132 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
133 use crate::protocol::metadata::*;
134 ProtocolMetadataV2::builder()
135 .state(DevelopmentState::Experimental)
136 .privilege_requirement(PrivilegeRequirement::None)
137 .implementation("MessagePack-RPC over Tokio TCP with the server's hand-written codec: pipelined calls matched by msgid, notifications both ways")
138 .llm_control("Which methods to call or notify, and what to do with each response and notification")
139 .e2e_testing("tests/client/msgpack_rpc: NetGet's own server; Neovim (--listen) and ugorji/go's MsgpackSpecRpc server as independent peers")
140 .notes("A request from the server is answered with an error: this client serves no methods. At most 64 calls may await their responses. Messages are capped at 1 MiB, nesting at 32 levels.")
141 .max_inbound_bytes(crate::server::msgpack_rpc::wire::MAX_MESSAGE)
142 .build()
143 }
144 fn example_prompt(&self) -> &'static str {
145 "Connect to Neovim's RPC socket at 127.0.0.1:6666 and evaluate 1 + 2"
146 }
147 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
148 let llm = json!({"type":"open_client","protocol":"msgpack-rpc","remote_addr":"127.0.0.1:6666","instruction":"Ask Neovim for its version"});
149 let mut static_example = llm.clone();
150 static_example["event_handlers"] = json!([
151 {"event_pattern":"msgpack_connected","handler":{"type":"static","actions":[{"type":"msgpack_call","method":"nvim_eval","params":["v:version"]}]}},
152 {"event_pattern":"msgpack_response","handler":{"type":"static","actions":[{"type":"disconnect"}]}}
153 ]);
154 let mut scripted = static_example.clone();
155 scripted["event_handlers"][1]["handler"] = json!({"type":"script","language":"python","code":"import json,sys\ne=json.load(sys.stdin)['event']\nprint(json.dumps({'actions':[{'type':'msgpack_notify','method':'log','params':[e['result']]},{'type':'disconnect'}]}))"});
156 crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
157 }
158 fn group_name(&self) -> &'static str {
159 "Application"
160 }
161}
162
163impl Client for MsgpackRpcClientProtocol {
164 fn connect(
165 &self,
166 ctx: ConnectContext,
167 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
168 {
169 Box::pin(super::connect(ctx))
170 }
171 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
172 if v["type"] == "disconnect" {
173 return Ok(ClientActionResult::Disconnect);
174 }
175 check(&v)?;
176 Ok(ClientActionResult::Custom {
177 name: v["type"].as_str().unwrap_or_default().to_string(),
178 data: v,
179 })
180 }
181}