Skip to main content

netget/client/msgpack_rpc/
actions.rs

1use 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}