Skip to main content

netget/client/capnp_rpc/
actions.rs

1use crate::llm::actions::{
2    client_trait::{Client, ClientActionResult},
3    protocol_trait::Protocol,
4    ActionDefinition, ParameterDefinition, StartupExamples,
5};
6use crate::protocol::{ConnectContext, EventType};
7use crate::server::capnp_rpc::actions::{action, parameter, schema_parameters, VALUE_NOTE};
8use crate::state::app_state::AppState;
9use anyhow::{bail, ensure, Result};
10use serde_json::{json, Value};
11use std::sync::LazyLock;
12
13#[derive(Default)]
14pub struct CapnpRpcClientProtocol;
15impl CapnpRpcClientProtocol {
16    pub fn new() -> Self {
17        Self
18    }
19}
20
21fn call_action() -> ActionDefinition {
22    action(
23        "capnp_call",
24        "Call a method of the server's bootstrap capability; the answer arrives as capnp_result.",
25        vec![
26            parameter(
27                "method",
28                "string",
29                "Method name, one of capnp_connected's methods",
30                true,
31            ),
32            parameter("params", "object", VALUE_NOTE, true),
33        ],
34        json!({"type":"capnp_call","method":"add","params":{"a":2,"b":3}}),
35        "-> Cap'n Proto call {method} {preview(params,80)}",
36    )
37}
38
39fn disconnect_action() -> ActionDefinition {
40    action(
41        "disconnect",
42        "Close the Cap'n Proto RPC connection",
43        vec![],
44        json!({"type":"disconnect"}),
45        "-> Cap'n Proto disconnect",
46    )
47}
48
49fn actions() -> Vec<ActionDefinition> {
50    vec![call_action(), disconnect_action()]
51}
52
53pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
54    EventType::new(
55        "capnp_connected",
56        "Connected and bootstrapped: the server's capability is ready to call.",
57        call_action().example.clone(),
58    )
59    .with_parameters(vec![
60        parameter("remote_addr", "string", "Server address and port", true),
61        parameter("interface", "string", "The bootstrap interface", true),
62        parameter(
63            "methods",
64            "array",
65            "Each method's name with its params and results fields",
66            true,
67        ),
68    ])
69    .with_actions(actions())
70});
71
72pub static RESULT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
73    EventType::new(
74        "capnp_result",
75        "The server answered a call with results or an exception.",
76        call_action().example.clone(),
77    )
78    .with_parameters(vec![
79        parameter("method", "string", "The method that was called", true),
80        parameter(
81            "results",
82            "object|null",
83            "The results struct as JSON, null on an exception",
84            true,
85        ),
86        parameter(
87            "exception",
88            "object|null",
89            "{reason, kind} when the call failed, otherwise null",
90            true,
91        ),
92    ])
93    .with_actions(actions())
94});
95
96impl Protocol for CapnpRpcClientProtocol {
97    fn protocol_name(&self) -> &'static str {
98        "Cap'n Proto RPC"
99    }
100    fn stack_name(&self) -> &'static str {
101        "ETH>IP>TCP>CapnProtoRPC"
102    }
103    fn keywords(&self) -> Vec<&'static str> {
104        vec!["capnp", "cap'n proto", "capnproto", "capnp-rpc"]
105    }
106    fn description(&self) -> &'static str {
107        "Cap'n Proto RPC client: bootstraps the server's capability and calls its methods, typed by a schema"
108    }
109    fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
110        actions()
111    }
112    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
113        vec![]
114    }
115    fn get_event_types(&self) -> Vec<EventType> {
116        vec![CONNECTED_EVENT.clone(), RESULT_EVENT.clone()]
117    }
118    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
119        schema_parameters()
120    }
121    fn get_dependencies(&self) -> Vec<crate::protocol::dependencies::ProtocolDependency> {
122        let mut deps =
123            crate::llm::actions::protocol_trait::default_dependencies_from_privilege(self);
124        deps.push(crate::protocol::dependencies::ProtocolDependency::ToolInPath("capnp"));
125        deps
126    }
127    fn startup_dependencies(
128        &self,
129        startup_params: Option<&Value>,
130    ) -> Vec<crate::protocol::dependencies::ProtocolDependency> {
131        let mut deps = self.get_dependencies();
132        if !crate::server::capnp_rpc::needs_compiler(startup_params) {
133            deps.retain(|d| {
134                *d != crate::protocol::dependencies::ProtocolDependency::ToolInPath("capnp")
135            });
136        }
137        deps
138    }
139    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
140        use crate::protocol::metadata::*;
141        ProtocolMetadataV2::builder()
142            .state(DevelopmentState::Experimental)
143            .privilege_requirement(PrivilegeRequirement::None)
144            .implementation("The server's hand-written Cap'n Proto encoding and RPC messages: Bootstrap, pipelined Calls on the bootstrap capability matched by question id, Return, Finish")
145            .llm_control("Which methods to call with which parameters, and what to do with each result")
146            .e2e_testing("tests/client/capnp_rpc: NetGet's own server; pycapnp (the C++ Cap'n Proto runtime) and capnproto.org/go/capnp v3 as independent servers")
147            .notes("Only the bootstrap capability is called; capabilities in results are not followed. The client exports nothing, so a call from the server is answered with an exception. At most 64 calls may await answers; a handler chain stops after 8 follow-ups. Same message bounds as the server.")
148            .max_inbound_bytes(crate::server::capnp_rpc::layout::MAX_MESSAGE_BYTES)
149            .build()
150    }
151    fn example_prompt(&self) -> &'static str {
152        "Connect to the Cap'n Proto RPC server at 127.0.0.1:5923 (a Directory interface) and look up readme"
153    }
154    fn get_startup_examples(&self) -> StartupExamples {
155        let llm = json!({"type":"open_client","protocol":"capnp-rpc","remote_addr":"127.0.0.1:5923","startup_params":{"schema":crate::server::capnp_rpc::actions::EXAMPLE_SCHEMA,"interface":"Directory"},"instruction":"Look up the entry named readme"});
156        let mut static_example = llm.clone();
157        static_example["event_handlers"] = json!([
158            {"event_pattern":"capnp_connected","handler":{"type":"static","actions":[{"type":"capnp_call","method":"lookup","params":{"name":"readme"}}]}},
159            {"event_pattern":"capnp_result","handler":{"type":"static","actions":[{"type":"disconnect"}]}}
160        ]);
161        let mut scripted = static_example.clone();
162        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':'disconnect'}] if e['exception'] else []}))"});
163        StartupExamples::new(llm, scripted, static_example)
164    }
165    fn group_name(&self) -> &'static str {
166        "Application"
167    }
168}
169
170impl Client for CapnpRpcClientProtocol {
171    fn connect(
172        &self,
173        ctx: ConnectContext,
174    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
175    {
176        Box::pin(super::connect(ctx))
177    }
178    fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
179        let name = v["type"].as_str().unwrap_or_default().to_string();
180        match name.as_str() {
181            "disconnect" => return Ok(ClientActionResult::Disconnect),
182            "capnp_call" => {
183                ensure!(
184                    v["method"].as_str().is_some_and(|m| !m.is_empty()),
185                    "method must be a non-empty string"
186                );
187                ensure!(
188                    v["params"].is_object() || v["params"].is_null(),
189                    "params must be an object"
190                );
191            }
192            _ => bail!("Unknown Cap'n Proto RPC client action"),
193        }
194        Ok(ClientActionResult::Custom { name, data: v })
195    }
196}