deepstrike_sdk/runtime/
remote_vpc_plane.rs1use std::collections::HashSet;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use async_stream::try_stream;
6use deepstrike_core::types::message::{ToolCall, ToolSchema};
7use futures::StreamExt;
8use futures::stream::Stream;
9
10use crate::Result;
11use crate::run_event::RunEvent;
12use crate::runtime::credential_vault::CredentialVault;
13use crate::runtime::execution_plane::{ExecutionPlane, LocalExecutionPlane, RunContext};
14use crate::tools::RegisteredTool;
15
16pub struct RemoteVpcOptions {
17 pub base_url: String,
21 pub vault: Arc<dyn CredentialVault>,
22 pub auth_credential_key: Option<String>,
25 pub schemas: Vec<ToolSchema>,
27 pub timeout_ms: u64,
29}
30
31pub struct RemoteVpcPlane {
39 base_url: String,
40 vault: Arc<dyn CredentialVault>,
41 auth_key: Option<String>,
42 remote_schemas: Vec<ToolSchema>,
43 client: reqwest::Client,
44 local: LocalExecutionPlane,
45}
46
47impl RemoteVpcPlane {
48 pub fn new(opts: RemoteVpcOptions) -> Self {
49 let client = reqwest::Client::builder()
50 .timeout(std::time::Duration::from_millis(opts.timeout_ms))
51 .build()
52 .unwrap_or_default();
53 Self {
54 base_url: opts.base_url.trim_end_matches('/').to_string(),
55 vault: opts.vault,
56 auth_key: opts.auth_credential_key,
57 remote_schemas: opts.schemas,
58 client,
59 local: LocalExecutionPlane::new(),
60 }
61 }
62
63 pub fn register(&mut self, tool: RegisteredTool) -> &mut Self {
65 self.local.register(tool);
66 self
67 }
68
69 pub fn unregister(&mut self, name: &str) -> &mut Self {
70 self.local.unregister(name);
71 self
72 }
73}
74
75impl ExecutionPlane for RemoteVpcPlane {
76 fn schemas(&self) -> Vec<ToolSchema> {
77 let local_names: HashSet<_> = self
78 .local
79 .schemas()
80 .into_iter()
81 .map(|s| s.name.clone())
82 .collect();
83 let mut result = self.local.schemas();
84 result.extend(
85 self.remote_schemas
86 .iter()
87 .filter(|s| !local_names.contains(&s.name))
88 .cloned(),
89 );
90 result
91 }
92
93 fn execute_all<'a>(
94 &'a self,
95 calls: &'a [ToolCall],
96 ctx: RunContext<'a>,
97 ) -> Pin<Box<dyn Stream<Item = Result<RunEvent>> + Send + 'a>> {
98 Box::pin(try_stream! {
99 let RunContext {
100 agent_id, skill_dir, dream_store, knowledge_source, governance, on_tool_suspend, on_permission_request,
101 } = ctx;
102
103 let local_names: HashSet<String> =
104 self.local.schemas().into_iter().map(|s| s.name.to_string()).collect();
105 let local_calls: Vec<_> =
106 calls.iter().filter(|c| local_names.contains(c.name.as_str())).cloned().collect();
107 let remote_calls: Vec<_> =
108 calls.iter().filter(|c| !local_names.contains(c.name.as_str())).cloned().collect();
109
110 if !local_calls.is_empty() {
111 let local_ctx = RunContext {
112 agent_id,
113 skill_dir,
114 dream_store,
115 knowledge_source,
116 governance: governance.clone(),
117 on_tool_suspend: on_tool_suspend.clone(),
118 on_permission_request: on_permission_request.clone(),
119 };
120 let mut s = self.local.execute_all(&local_calls, local_ctx);
121 while let Some(evt) = s.next().await {
122 yield evt?;
123 }
124 }
125
126 if !remote_calls.is_empty() {
127 let auth = if let Some(key) = &self.auth_key {
128 self.vault.get(key).await
129 } else {
130 None
131 };
132
133 let futs: Vec<_> = remote_calls
135 .iter()
136 .map(|call| {
137 let client = self.client.clone();
138 let url = format!("{}/execute", self.base_url);
139 let auth = auth.clone();
140 let call = call.clone();
141 async move { call_remote(client, url, auth, call).await }
142 })
143 .collect();
144
145 let results = futures::future::join_all(futs).await;
146 for (call, (content, is_error)) in remote_calls.iter().zip(results) {
147 yield RunEvent::ToolResult {
148 call_id: call.id.to_string(),
149 content,
150 is_error,
151 is_fatal: false,
152 error_kind: None,
153 };
154 }
155 }
156 })
157 }
158}
159
160async fn call_remote(
163 client: reqwest::Client,
164 url: String,
165 auth: Option<String>,
166 call: ToolCall,
167) -> (String, bool) {
168 let mut req = client.post(&url).json(&serde_json::json!({
169 "name": call.name.as_str(),
170 "arguments": call.arguments,
171 }));
172 if let Some(token) = auth {
173 req = req.header("Authorization", token);
174 }
175
176 match req.send().await {
177 Ok(resp) => {
178 if !resp.status().is_success() {
179 let status = resp.status().as_u16();
180 let body = resp.text().await.unwrap_or_default();
181 let msg = if body.is_empty() {
182 format!("HTTP {status}")
183 } else {
184 format!("HTTP {status}: {body}")
185 };
186 return (msg, true);
187 }
188 match resp.json::<serde_json::Value>().await {
189 Ok(result) => {
190 let output = result
191 .get("output")
192 .and_then(|v| v.as_str())
193 .unwrap_or("")
194 .to_string();
195 let is_error = result
196 .get("isError")
197 .and_then(|v| v.as_bool())
198 .unwrap_or(false);
199 (output, is_error)
200 }
201 Err(e) => (e.to_string(), true),
202 }
203 }
204 Err(e) => (e.to_string(), true),
205 }
206}