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}