1pub mod contract;
2pub mod learn;
3
4pub mod config;
8pub mod error;
9pub mod transport;
10pub mod domains;
11pub mod helpers;
12
13pub mod proto {
14 pub mod mubit {
15 pub mod v1 {
16 tonic::include_proto!("mubit.v1");
17 }
18 }
19}
20
21use crate::contract::{find_operation, HttpMethod, OperationSpec};
22use reqwest::header::CONTENT_TYPE;
23use serde::de::DeserializeOwned;
24use serde::Serialize;
25use serde_json::{json, Map, Value};
26use std::collections::HashSet;
27use std::pin::Pin;
28use std::sync::{Arc, RwLock};
29use std::time::{Duration, Instant};
30use thiserror::Error;
31use tokio::sync::Mutex;
32use tokio::time::sleep;
33use tokio_stream::{Stream, StreamExt};
34use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
35use tonic::{Code, Request, Status};
36use url::Url;
37
38pub type ValueStream = Pin<Box<dyn Stream<Item = Result<Value>> + Send>>;
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum TransportMode {
43 Auto,
44 Grpc,
45 Http,
46}
47
48impl TransportMode {
49 fn normalize(raw: &str) -> Self {
50 match raw.trim().to_lowercase().as_str() {
51 "grpc" => Self::Grpc,
52 "http" => Self::Http,
53 _ => Self::Auto,
54 }
55 }
56}
57
58const DEFAULT_SHARED_HTTP_ENDPOINT: &str = "https://api.mubit.ai";
59const DEFAULT_SHARED_GRPC_ENDPOINT: &str = "grpc.api.mubit.ai:443";
60
61fn env_non_empty(name: &str) -> Option<String> {
62 std::env::var(name)
63 .ok()
64 .map(|value| value.trim().to_string())
65 .filter(|value| !value.is_empty())
66}
67
68#[derive(Clone, Debug)]
69pub struct ClientConfig {
70 pub endpoint: String,
71 pub grpc_endpoint: Option<String>,
72 pub http_endpoint: Option<String>,
73 pub transport: TransportMode,
74 pub api_key: Option<String>,
75 pub token: Option<String>, pub run_id: Option<String>,
77 pub timeout_ms: u64,
78}
79
80impl ClientConfig {
81 pub fn new(endpoint: impl Into<String>) -> Self {
82 Self {
83 endpoint: endpoint.into(),
84 grpc_endpoint: None,
85 http_endpoint: None,
86 transport: TransportMode::Auto,
87 api_key: None,
88 token: None,
89 run_id: None,
90 timeout_ms: 30_000,
91 }
92 }
93
94 pub fn transport(mut self, transport: impl AsRef<str>) -> Self {
95 self.transport = TransportMode::normalize(transport.as_ref());
96 self
97 }
98
99 pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
100 self.api_key = Some(api_key.into());
101 self
102 }
103
104 pub fn token(mut self, token: impl Into<String>) -> Self {
105 self.api_key = Some(token.into());
106 self
107 }
108
109 pub fn run_id(mut self, run_id: impl Into<String>) -> Self {
110 self.run_id = Some(run_id.into());
111 self
112 }
113
114 pub fn from_env() -> Self {
115 Self::default()
116 }
117}
118
119impl Default for ClientConfig {
120 fn default() -> Self {
121 let transport = env_non_empty("MUBIT_TRANSPORT")
122 .map(|value| TransportMode::normalize(&value))
123 .unwrap_or(TransportMode::Auto);
124 let endpoint = env_non_empty("MUBIT_ENDPOINT")
125 .unwrap_or_else(|| DEFAULT_SHARED_HTTP_ENDPOINT.to_string());
126
127 let mut config = Self::new(endpoint);
128 config.transport = transport;
129 config.http_endpoint = env_non_empty("MUBIT_HTTP_ENDPOINT");
130 config.grpc_endpoint = env_non_empty("MUBIT_GRPC_ENDPOINT");
131 config.api_key = env_non_empty("MUBIT_API_KEY");
132 config.token = env_non_empty("MUBIT_TOKEN");
133 config.run_id = env_non_empty("MUBIT_RUN_ID");
134
135 if config.http_endpoint.is_none() {
136 config.http_endpoint = Some(DEFAULT_SHARED_HTTP_ENDPOINT.to_string());
137 }
138 if config.grpc_endpoint.is_none() {
139 config.grpc_endpoint = Some(DEFAULT_SHARED_GRPC_ENDPOINT.to_string());
140 }
141
142 config
143 }
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
147pub enum TransportFailureKind {
148 Unavailable,
149 ConnectionReset,
150 DeadlineExceeded,
151 Io,
152 Unimplemented,
153 Other,
154}
155
156#[derive(Error, Debug)]
157pub enum SdkError {
158 #[error("AuthError: {0}")]
159 AuthError(String),
160 #[error("ValidationError: {0}")]
161 ValidationError(String),
162 #[error("TransportError({kind:?}): {message}")]
163 TransportError {
164 kind: TransportFailureKind,
165 message: String,
166 },
167 #[error("ServerError: {0}")]
168 ServerError(String),
169 #[error("UnsupportedFeatureError: {0}")]
170 UnsupportedFeatureError(String),
171}
172
173impl SdkError {
174 fn is_fallback_eligible(&self) -> bool {
175 matches!(
176 self,
177 SdkError::TransportError {
178 kind: TransportFailureKind::Unavailable
179 | TransportFailureKind::ConnectionReset
180 | TransportFailureKind::DeadlineExceeded
181 | TransportFailureKind::Io
182 | TransportFailureKind::Unimplemented,
183 ..
184 }
185 )
186 }
187
188 fn is_retryable(&self) -> bool {
191 match self {
192 SdkError::TransportError { kind, .. } => matches!(
193 kind,
194 TransportFailureKind::Unavailable
195 | TransportFailureKind::ConnectionReset
196 | TransportFailureKind::DeadlineExceeded
197 | TransportFailureKind::Io
198 ),
199 SdkError::ServerError(_) => true,
200 SdkError::AuthError(_)
201 | SdkError::ValidationError(_)
202 | SdkError::UnsupportedFeatureError(_) => false,
203 }
204 }
205}
206
207fn retry_env_u64(name: &str, default: u64, minimum: u64) -> u64 {
212 std::env::var(name)
213 .ok()
214 .and_then(|v| v.trim().parse::<u64>().ok())
215 .map(|v| v.max(minimum))
216 .unwrap_or(default)
217}
218
219fn retry_env_f64(name: &str, default: f64, minimum: f64) -> f64 {
220 std::env::var(name)
221 .ok()
222 .and_then(|v| v.trim().parse::<f64>().ok())
223 .map(|v| v.max(minimum))
224 .unwrap_or(default)
225}
226
227fn retry_attempts() -> u32 {
228 retry_env_u64("MUBIT_RETRY_ATTEMPTS", 3, 1) as u32
229}
230
231fn retry_base_ms() -> u64 {
232 retry_env_u64("MUBIT_RETRY_BASE_MS", 200, 10)
233}
234
235fn retry_cap_ms() -> u64 {
236 retry_env_u64("MUBIT_RETRY_CAP_MS", 5000, retry_base_ms())
237}
238
239fn retry_jitter() -> f64 {
240 retry_env_f64("MUBIT_RETRY_JITTER", 0.2, 0.0)
241}
242
243fn backoff_delay_ms(attempt: u32) -> Duration {
244 if attempt <= 1 {
245 return Duration::ZERO;
246 }
247 let base = retry_base_ms();
248 let cap = retry_cap_ms();
249 let exp = std::cmp::min(base.saturating_mul(1u64 << (attempt - 2)), cap) as f64;
250 let j = retry_jitter();
251 let ms = if j > 0.0 {
252 let nanos = std::time::SystemTime::now()
254 .duration_since(std::time::UNIX_EPOCH)
255 .map(|d| d.subsec_nanos())
256 .unwrap_or(0);
257 let frac = (nanos as f64 / 1_000_000_000.0) * 2.0 - 1.0;
258 (exp * (1.0 + frac * j)).max(0.0) as u64
259 } else {
260 exp as u64
261 };
262 Duration::from_millis(ms)
263}
264
265pub type Result<T> = std::result::Result<T, SdkError>;
266
267#[derive(Clone, Debug)]
268struct MutableState {
269 api_key: Option<String>,
270 run_id: Option<String>,
271 transport: TransportMode,
272}
273
274struct TransportEngine {
275 http_endpoint: String,
276 grpc_endpoint: String,
277 grpc_tls: bool,
278 timeout: Duration,
279 http_client: reqwest::Client,
280 grpc_channel: Mutex<Option<Channel>>,
281 state: Arc<RwLock<MutableState>>,
282}
283
284impl TransportEngine {
285 fn new(config: ClientConfig) -> Result<Self> {
286 let (default_http_endpoint, default_grpc_endpoint, default_grpc_tls) =
287 derive_http_and_grpc(&config.endpoint)?;
288
289 let http_endpoint = match config.http_endpoint {
290 Some(http_endpoint) => normalize_http_endpoint(&http_endpoint)?,
291 None => default_http_endpoint,
292 };
293
294 let (grpc_endpoint, grpc_tls) = match config.grpc_endpoint {
295 Some(grpc_endpoint) => normalize_grpc_endpoint(&grpc_endpoint)?,
296 None => (default_grpc_endpoint, default_grpc_tls),
297 };
298
299 let timeout = Duration::from_millis(config.timeout_ms);
300 let http_client = reqwest::Client::builder()
301 .timeout(timeout)
302 .build()
303 .map_err(|e| SdkError::TransportError {
304 kind: TransportFailureKind::Other,
305 message: format!("failed to build HTTP client: {}", e),
306 })?;
307
308 Ok(Self {
309 http_endpoint,
310 grpc_endpoint,
311 grpc_tls,
312 timeout,
313 http_client,
314 grpc_channel: Mutex::new(None),
315 state: Arc::new(RwLock::new(MutableState {
316 api_key: config.api_key.or(config.token),
317 run_id: config.run_id,
318 transport: config.transport,
319 })),
320 })
321 }
322
323 fn set_api_key(&self, api_key: Option<String>) {
324 if let Ok(mut state) = self.state.write() {
325 state.api_key = api_key;
326 }
327 }
328
329 fn set_run_id(&self, run_id: Option<String>) {
330 if let Ok(mut state) = self.state.write() {
331 state.run_id = run_id;
332 }
333 }
334
335 fn set_transport(&self, transport: TransportMode) {
336 if let Ok(mut state) = self.state.write() {
337 state.transport = transport;
338 }
339 }
340
341 fn api_key(&self) -> Option<String> {
342 self.state.read().ok().and_then(|s| s.api_key.clone())
343 }
344
345 fn run_id(&self) -> Option<String> {
346 self.state.read().ok().and_then(|s| s.run_id.clone())
347 }
348
349 fn transport(&self) -> TransportMode {
350 self.state
351 .read()
352 .map(|s| s.transport)
353 .unwrap_or(TransportMode::Auto)
354 }
355
356 async fn invoke_serialized<T: Serialize>(&self, op_key: &str, payload: T) -> Result<Value> {
357 let value = serde_json::to_value(payload).map_err(|e| {
358 SdkError::ValidationError(format!("failed to serialize request payload: {}", e))
359 })?;
360 self.invoke(op_key, value).await
361 }
362
363 async fn invoke(&self, op_key: &str, payload: Value) -> Result<Value> {
364 let op = find_operation(op_key).ok_or_else(|| {
365 SdkError::UnsupportedFeatureError(format!("unknown operation: {}", op_key))
366 })?;
367
368 let mut payload = ensure_object_payload(payload)?;
369 self.apply_run_id_default(op, &mut payload);
370 let transport_mode = self.transport();
371
372 let attempts = retry_attempts();
373 let mut last_err: Option<SdkError> = None;
374 for attempt in 1..=attempts {
375 if attempt > 1 {
376 let delay = backoff_delay_ms(attempt);
377 if !delay.is_zero() {
378 tokio::time::sleep(delay).await;
379 }
380 }
381 let result = match transport_mode {
382 TransportMode::Grpc => self.invoke_grpc(op, payload.clone()).await,
383 TransportMode::Http => self.invoke_http(op, payload.clone()).await,
384 TransportMode::Auto => match self.invoke_grpc(op, payload.clone()).await {
385 Ok(value) => Ok(value),
386 Err(err) if err.is_fallback_eligible() => {
387 self.invoke_http(op, payload.clone()).await
388 }
389 Err(err) => Err(err),
390 },
391 };
392 match result {
393 Ok(value) => return Ok(value),
394 Err(err) => {
395 if !err.is_retryable() || attempt >= attempts {
396 return Err(err);
397 }
398 last_err = Some(err);
399 }
400 }
401 }
402 Err(last_err.unwrap_or_else(|| SdkError::TransportError {
403 kind: TransportFailureKind::Other,
404 message: "retry loop exited without result".to_string(),
405 }))
406 }
407
408 pub async fn invoke_stream(&self, key: &str, payload: Value) -> Result<ValueStream> {
409 let op = find_operation(key).ok_or_else(|| {
410 SdkError::UnsupportedFeatureError(format!("unknown operation: {}", key))
411 })?;
412
413 let mut payload = ensure_object_payload(payload)?;
414 self.apply_run_id_default(op, &mut payload);
415
416 match self.transport() {
417 TransportMode::Grpc => self.invoke_grpc_stream(op, payload).await,
418 TransportMode::Http => self.invoke_http_stream(op, payload).await,
419 TransportMode::Auto => match self.invoke_grpc_stream(op, payload.clone()).await {
420 Ok(stream) => Ok(stream),
421 Err(err) if err.is_fallback_eligible() => {
422 self.invoke_http_stream(op, payload).await
423 }
424 Err(err) => Err(err),
425 },
426 }
427 }
428
429 pub async fn invoke_stream_serialized<T: Serialize>(
430 &self,
431 key: &str,
432 payload: T,
433 ) -> Result<ValueStream> {
434 let value = serde_json::to_value(payload).map_err(|e| {
435 SdkError::ValidationError(format!("failed to serialize payload: {}", e))
436 })?;
437 self.invoke_stream(key, value).await
438 }
439
440 fn apply_run_id_default(&self, op: &OperationSpec, payload: &mut Value) {
441 let Some(run_id_field) = op.run_id_field else {
442 return;
443 };
444 let Some(run_id) = self.run_id() else {
445 return;
446 };
447
448 if let Some(map) = payload.as_object_mut() {
449 if !map.contains_key(run_id_field) {
450 map.insert(run_id_field.to_string(), Value::String(run_id));
451 }
452 }
453 }
454
455 async fn grpc_channel(&self) -> Result<Channel> {
456 {
457 let guard = self.grpc_channel.lock().await;
458 if let Some(channel) = guard.as_ref() {
459 return Ok(channel.clone());
460 }
461 }
462
463 let uri = if self.grpc_tls {
464 format!("https://{}", self.grpc_endpoint)
465 } else {
466 format!("http://{}", self.grpc_endpoint)
467 };
468
469 let mut endpoint = Endpoint::from_shared(uri.clone()).map_err(|e| {
470 SdkError::ValidationError(format!("invalid gRPC endpoint '{}': {}", uri, e))
471 })?;
472 endpoint = endpoint.connect_timeout(self.timeout).timeout(self.timeout);
473 if self.grpc_tls {
474 endpoint = endpoint.tls_config(ClientTlsConfig::new()).map_err(|e| {
475 SdkError::TransportError {
476 kind: TransportFailureKind::Other,
477 message: format!("failed to configure TLS for {}: {}", self.grpc_endpoint, e),
478 }
479 })?;
480 }
481
482 let channel = endpoint
483 .connect()
484 .await
485 .map_err(|e| map_grpc_connect_error(e, &self.grpc_endpoint))?;
486
487 let mut guard = self.grpc_channel.lock().await;
488 *guard = Some(channel.clone());
489 Ok(channel)
490 }
491
492 fn attach_grpc_metadata<T>(&self, request: &mut Request<T>) {
493 if let Some(api_key) = self.api_key() {
494 if let Ok(header_value) = format!("Bearer {}", api_key).parse() {
495 request.metadata_mut().insert("authorization", header_value);
496 }
497 }
498 }
499
500 fn grpc_request<T>(&self, payload: T) -> Request<T> {
501 let mut request = Request::new(payload);
502 self.attach_grpc_metadata(&mut request);
503 request
504 }
505
506 async fn invoke_grpc(&self, op: &OperationSpec, payload: Value) -> Result<Value> {
507 use crate::proto::mubit::v1 as pb;
508
509 if op.grpc_method.is_empty() {
510 return Err(SdkError::TransportError {
511 kind: TransportFailureKind::Unimplemented,
512 message: format!("operation {} has no gRPC mapping", op.key),
513 });
514 }
515
516 let channel = self.grpc_channel().await?;
517
518 macro_rules! unary_core {
519 ($method:ident, $req_ty:ty) => {{
520 let request: $req_ty = decode_grpc_request(op.key, payload)?;
521 let mut client = pb::core_service_client::CoreServiceClient::new(channel.clone());
522 let response = client
523 .$method(self.grpc_request(request))
524 .await
525 .map_err(map_grpc_status)?;
526 encode_grpc_response(op.key, response.into_inner())
527 }};
528 }
529
530 macro_rules! unary_control {
531 ($method:ident, $req_ty:ty) => {{
532 let request: $req_ty = decode_grpc_request(op.key, payload)?;
533 let mut client =
534 pb::control_service_client::ControlServiceClient::new(channel.clone());
535 let response = client
536 .$method(self.grpc_request(request))
537 .await
538 .map_err(map_grpc_status)?;
539 encode_grpc_response(op.key, response.into_inner())
540 }};
541 }
542
543 match op.key {
544 "auth.health" => unary_core!(health, pb::HealthRequest),
546 "auth.create_user" => unary_core!(create_user, pb::CreateUserRequest),
547 "auth.rotate_user_api_key" => {
548 unary_core!(rotate_user_api_key, pb::RotateUserApiKeyRequest)
549 }
550 "auth.revoke_user_api_key" => {
551 unary_core!(revoke_user_api_key, pb::RevokeUserApiKeyRequest)
552 }
553 "auth.list_users" => unary_core!(list_users, pb::ListUsersRequest),
554 "auth.get_user" => unary_core!(get_user, pb::GetUserRequest),
555 "auth.delete_user" => unary_core!(delete_user, pb::DeleteUserRequest),
556
557 "core.insert" => unary_core!(insert, pb::InsertRequest),
559 "core.search" => unary_core!(search, pb::SearchRequest),
560 "core.delete_node" => unary_core!(delete_node, pb::DeleteNodeRequest),
561 "core.delete_run" => unary_core!(delete_run, pb::DeleteRunRequest),
562 "core.create_session" => unary_core!(create_session, pb::CreateSessionRequest),
563 "core.snapshot_session" => unary_core!(snapshot_session, pb::SnapshotSessionRequest),
564 "core.load_session" => unary_core!(load_session, pb::LoadSessionRequest),
565 "core.commit_session" => unary_core!(commit_session, pb::CommitSessionRequest),
566 "core.drop_session" => unary_core!(drop_session, pb::DropSessionRequest),
567 "core.write_memory" => unary_core!(write_memory, pb::WriteMemoryRequest),
568 "core.read_memory" => unary_core!(read_memory, pb::ReadMemoryRequest),
569 "core.add_memory" => unary_core!(add_memory, pb::AddMemoryRequest),
570 "core.get_memory" => unary_core!(get_memory, pb::GetMemoryRequest),
571 "core.clear_memory" => unary_core!(clear_memory, pb::ClearMemoryRequest),
572 "core.grant_permission" => unary_core!(grant_permission, pb::GrantPermissionRequest),
573 "core.revoke_permission" => unary_core!(revoke_permission, pb::RevokePermissionRequest),
574 "core.batch_insert" => {
575 let request_items = decode_batch_insert_payload(payload)?;
576 let mut request = Request::new(tokio_stream::iter(request_items));
577 self.attach_grpc_metadata(&mut request);
578 let mut client = pb::core_service_client::CoreServiceClient::new(channel.clone());
579 let response = client
580 .batch_insert(request)
581 .await
582 .map_err(map_grpc_status)?;
583 encode_grpc_response(op.key, response.into_inner())
584 }
585 "control.set_variable" => unary_control!(set_variable, pb::SetVariableRequest),
587 "control.get_variable" => unary_control!(get_variable, pb::GetVariableRequest),
588 "control.list_variables" => unary_control!(list_variables, pb::ListVariablesRequest),
589 "control.delete_variable" => unary_control!(delete_variable, pb::DeleteVariableRequest),
590 "control.define_concept" => unary_control!(define_concept, pb::DefineConceptRequest),
591 "control.list_concepts" => unary_control!(list_concepts, pb::ListConceptsRequest),
592 "control.add_goal" => unary_control!(add_goal, pb::AddGoalRequest),
593 "control.update_goal" => unary_control!(update_goal, pb::UpdateGoalRequest),
594 "control.list_goals" => unary_control!(list_goals, pb::ListGoalsRequest),
595 "control.get_goal_tree" => unary_control!(get_goal_tree, pb::GetGoalTreeRequest),
596 "control.submit_action" => unary_control!(submit_action, pb::ActionRequest),
597 "control.get_action_log" => unary_control!(get_action_log, pb::ActionLogRequest),
598 "control.run_cycle" => unary_control!(run_cycle, pb::RunCycleRequest),
599 "control.get_cycle_history" => {
600 unary_control!(get_cycle_history, pb::CycleHistoryRequest)
601 }
602 "control.register_agent" => unary_control!(register_agent, pb::AgentRegisterRequest),
603 "control.agent_heartbeat" => {
604 unary_control!(agent_heartbeat, pb::AgentHeartbeatRequest)
605 }
606 "control.append_activity" => unary_control!(append_activity, pb::ActivityAppendRequest),
607 "control.context_snapshot" => unary_control!(get_run_snapshot, pb::RunSnapshotRequest),
608 "control.link_run" => unary_control!(link_run, pb::LinkRunRequest),
609 "control.unlink_run" => unary_control!(unlink_run, pb::UnlinkRunRequest),
610 "control.ingest" => unary_control!(ingest, pb::IngestRequest),
611 "control.batch_insert" => {
612 unary_control!(batch_insert, pb::ControlBatchInsertRequest)
613 }
614 "control.get_ingest_job" => unary_control!(get_ingest_job, pb::GetIngestJobRequest),
615 "control.query" => unary_control!(query, pb::AgentQueryRequest),
616 "control.diagnose" => unary_control!(diagnose, pb::DiagnoseRequest),
617 "control.delete_run" => unary_control!(delete_run, pb::RunRequest),
618 "control.reflect" => unary_control!(reflect, pb::ReflectRequest),
619 "control.lessons" => unary_control!(list_lessons, pb::ListLessonsRequest),
620 "control.delete_lesson" => unary_control!(delete_lesson, pb::DeleteLessonRequest),
621 "control.context" => unary_control!(get_context, pb::ContextRequest),
622 "control.list_activity" => unary_control!(list_activity, pb::ListActivityRequest),
623 "control.export_activity" => {
624 unary_control!(export_activity, pb::ExportActivityRequest)
625 }
626 "control.archive_block" => unary_control!(archive_block, pb::ArchiveBlockRequest),
627 "control.dereference" => unary_control!(dereference, pb::DereferenceRequest),
628 "control.memory_health" => {
629 unary_control!(get_memory_health, pb::MemoryHealthRequest)
630 }
631 "control.checkpoint" => unary_control!(checkpoint, pb::CheckpointRequest),
632 "control.list_agents" => unary_control!(list_agents, pb::ListAgentsRequest),
633 "control.create_handoff" => unary_control!(create_handoff, pb::HandoffRequest),
634 "control.submit_feedback" => unary_control!(submit_feedback, pb::FeedbackRequest),
635 "control.record_outcome" => {
636 unary_control!(record_outcome, pb::RecordOutcomeRequest)
637 }
638 "control.surface_strategies" => {
639 unary_control!(surface_strategies, pb::SurfaceStrategiesRequest)
640 }
641 _ => Err(SdkError::UnsupportedFeatureError(format!(
642 "unknown gRPC operation: {}",
643 op.key
644 ))),
645 }
646 }
647
648 async fn invoke_grpc_stream(
649 &self,
650 op: &'static OperationSpec,
651 payload: Value,
652 ) -> Result<ValueStream> {
653 use crate::proto::mubit::v1 as pb;
654
655 let channel = self.grpc_channel().await?;
656
657 match op.key {
658 "core.subscribe_events" => {
659 let request: pb::CoreSubscribeRequest =
660 decode_grpc_request(op.key, payload)?;
661 let mut client =
662 pb::core_service_client::CoreServiceClient::new(channel.clone());
663 let response = client
664 .subscribe(self.grpc_request(request))
665 .await
666 .map_err(map_grpc_status)?;
667 let stream = response.into_inner();
668 Ok(Box::pin(stream.map(|result| {
669 result
670 .map(|msg| {
671 let mut value =
672 serde_json::to_value(msg).unwrap_or_else(|e| {
673 serde_json::json!({"error": e.to_string()})
674 });
675 hydrate_pubsub_event(&mut value);
676 value
677 })
678 .map_err(map_grpc_status)
679 })))
680 }
681 "control.subscribe" => {
682 let request: pb::SubscribeRequest =
683 decode_grpc_request(op.key, payload)?;
684 let mut client =
685 pb::control_service_client::ControlServiceClient::new(channel.clone());
686 let response = client
687 .subscribe(self.grpc_request(request))
688 .await
689 .map_err(map_grpc_status)?;
690 let stream = response.into_inner();
691 Ok(Box::pin(stream.map(|result| {
692 result
693 .map(|msg| {
694 serde_json::to_value(msg).unwrap_or_else(|e| {
695 serde_json::json!({"error": e.to_string()})
696 })
697 })
698 .map_err(map_grpc_status)
699 })))
700 }
701 "core.watch_memory" => {
702 let request: pb::WatchMemoryRequest =
703 decode_grpc_request(op.key, payload)?;
704 let mut client =
705 pb::core_service_client::CoreServiceClient::new(channel.clone());
706 let response = client
707 .watch_memory(self.grpc_request(request))
708 .await
709 .map_err(map_grpc_status)?;
710 let stream = response.into_inner();
711 Ok(Box::pin(stream.map(|result| {
712 result
713 .map(|msg| {
714 serde_json::to_value(msg).unwrap_or_else(|e| {
715 serde_json::json!({"error": e.to_string()})
716 })
717 })
718 .map_err(map_grpc_status)
719 })))
720 }
721 _ => Err(SdkError::UnsupportedFeatureError(format!(
722 "gRPC streaming not supported for {}",
723 op.key
724 ))),
725 }
726 }
727
728 async fn invoke_http_stream(
729 &self,
730 op: &'static OperationSpec,
731 payload: Value,
732 ) -> Result<ValueStream> {
733 let base = self.http_endpoint.trim_end_matches('/');
734 let route = op.http_path;
735 let url = format!("{}{}", base, route);
736
737 let client = &self.http_client;
738 let is_get = matches!(op.http_method, HttpMethod::Get);
739
740 let mut request = if is_get {
741 let mut req = client.get(&url);
742 if let Some(obj) = payload.as_object() {
743 for (k, v) in obj {
744 let val = match v {
745 Value::String(s) => s.clone(),
746 other => other.to_string(),
747 };
748 req = req.query(&[(k.as_str(), val)]);
749 }
750 }
751 req
752 } else {
753 client.post(&url).json(&payload)
754 };
755
756 if let Some(api_key) = self.api_key() {
757 request = request.bearer_auth(api_key);
758 }
759
760 let response = request.send().await.map_err(|e| {
761 map_transport_error(e, format!("{} SSE request failed", op.key))
762 })?;
763
764 let status = response.status();
765 if !status.is_success() {
766 let body = response
767 .text()
768 .await
769 .unwrap_or_else(|_| "request failed".to_string());
770 return Err(map_http_error(status.as_u16(), body));
771 }
772
773 let byte_stream = response.bytes_stream();
774 let sse_stream = parse_sse_byte_stream(byte_stream);
775
776 if op.key == "core.subscribe_events" {
781 let hydrated = sse_stream.map(|result| {
782 result.map(|mut value| {
783 hydrate_pubsub_event(&mut value);
784 value
785 })
786 });
787 Ok(Box::pin(hydrated))
788 } else {
789 Ok(Box::pin(sse_stream))
790 }
791 }
792
793 async fn invoke_http(&self, op: &OperationSpec, payload: Value) -> Result<Value> {
794 let mut path = op.http_path.to_string();
795 let mut consumed_keys = HashSet::new();
796
797 if let Some(map) = payload.as_object() {
798 for (key, value) in map {
799 let marker = format!(":{}", key);
800 if path.contains(&marker) {
801 let rendered = value_to_param(value).ok_or_else(|| {
802 SdkError::ValidationError(format!(
803 "invalid path parameter value for {} in {}",
804 key, op.key
805 ))
806 })?;
807 path = path.replace(&marker, &rendered);
808 consumed_keys.insert(key.clone());
809 }
810 }
811 }
812
813 if path.contains(':') {
814 return Err(SdkError::ValidationError(format!(
815 "missing path parameter for {}",
816 op.key
817 )));
818 }
819
820 let url = format!("{}{}", self.http_endpoint.trim_end_matches('/'), path);
821 let mut request = match op.http_method {
822 HttpMethod::Get => self.http_client.get(&url),
823 HttpMethod::Post => self.http_client.post(&url),
824 HttpMethod::Delete => self.http_client.delete(&url),
825 };
826
827 if let Some(api_key) = self.api_key() {
828 request = request.bearer_auth(api_key);
829 }
830
831 if matches!(op.http_method, HttpMethod::Get) {
832 let query = payload
833 .as_object()
834 .map(|map| {
835 map.iter()
836 .filter_map(|(key, value)| {
837 if consumed_keys.contains(key) || value.is_null() {
838 return None;
839 }
840 value_to_query(value).map(|rendered| (key.clone(), rendered))
841 })
842 .collect::<Vec<(String, String)>>()
843 })
844 .unwrap_or_default();
845
846 if !query.is_empty() {
847 request = request.query(&query);
848 }
849 } else if payload
850 .as_object()
851 .map(|map| !map.is_empty())
852 .unwrap_or(false)
853 {
854 request = request.json(&payload);
855 }
856
857 let response = request.send().await.map_err(|e| {
858 map_transport_error(
859 e,
860 format!(
861 "{} {} request failed",
862 http_method_label(op.http_method),
863 op.key
864 ),
865 )
866 })?;
867
868 let status = response.status();
869 if !status.is_success() {
870 let body = response
871 .text()
872 .await
873 .unwrap_or_else(|_| "request failed".to_string());
874 return Err(map_http_error(status.as_u16(), body));
875 }
876
877 let content_type = response
878 .headers()
879 .get(CONTENT_TYPE)
880 .and_then(|v| v.to_str().ok())
881 .map(|v| v.to_lowercase())
882 .unwrap_or_default();
883
884 let bytes = response
885 .bytes()
886 .await
887 .map_err(|e| SdkError::TransportError {
888 kind: TransportFailureKind::Io,
889 message: format!("failed to read response body for {}: {}", op.key, e),
890 })?;
891
892 if bytes.is_empty() {
893 return Ok(json!({}));
894 }
895
896 if content_type.contains("application/json") {
897 return serde_json::from_slice::<Value>(&bytes).map_err(|e| {
898 SdkError::ServerError(format!(
899 "failed to decode json response for {}: {}",
900 op.key, e
901 ))
902 });
903 }
904
905 Ok(Value::String(String::from_utf8_lossy(&bytes).to_string()))
906 }
907}
908
909#[derive(Clone)]
910pub struct Client {
911 pub auth: AuthClient,
912 pub core: CoreClient,
913 pub(crate) control: ControlClient,
914 transport: Arc<TransportEngine>,
915}
916
917impl Client {
918 pub fn new(config: ClientConfig) -> Result<Self> {
919 let transport = Arc::new(TransportEngine::new(config)?);
920 Ok(Self {
921 auth: AuthClient {
922 transport: transport.clone(),
923 },
924 core: CoreClient {
925 transport: transport.clone(),
926 },
927 control: ControlClient {
928 transport: transport.clone(),
929 },
930 transport,
931 })
932 }
933
934 pub fn set_api_key(&self, api_key: Option<String>) {
935 self.transport.set_api_key(api_key);
936 }
937
938 pub fn set_token(&self, token: Option<String>) {
939 self.set_api_key(token);
940 }
941
942 pub fn set_run_id(&self, run_id: Option<String>) {
943 self.transport.set_run_id(run_id);
944 }
945
946 pub fn set_transport(&self, transport: TransportMode) {
947 self.transport.set_transport(transport);
948 }
949}
950
951#[derive(Clone, Debug)]
952pub struct RememberOptions {
953 pub run_id: Option<String>,
954 pub agent_id: Option<String>,
955 pub item_id: Option<String>,
956 pub content: String,
957 pub content_type: String,
958 pub metadata: Option<Value>,
959 pub hints: Option<Value>,
960 pub payload: Option<Value>,
961 pub intent: Option<String>,
962 pub lesson_type: Option<String>,
963 pub lesson_scope: Option<String>,
964 pub lesson_importance: Option<String>,
965 pub lesson_conditions: Vec<String>,
966 pub user_id: Option<String>,
967 pub upsert_key: Option<String>,
968 pub importance: Option<String>,
969 pub source: Option<String>,
970 pub lane: Option<String>,
971 pub parallel: bool,
972 pub idempotency_key: Option<String>,
973 pub wait: bool,
974 pub timeout_ms: Option<u64>,
975 pub poll_interval_ms: u64,
976 pub occurrence_time: Option<i64>,
980}
981
982impl RememberOptions {
983 pub fn new(content: impl Into<String>) -> Self {
984 Self {
985 run_id: None,
986 agent_id: Some("sdk-client".to_string()),
987 item_id: None,
988 content: content.into(),
989 content_type: "text/plain".to_string(),
990 metadata: None,
991 hints: None,
992 payload: None,
993 intent: None,
994 lesson_type: None,
995 lesson_scope: None,
996 lesson_importance: None,
997 lesson_conditions: Vec::new(),
998 user_id: None,
999 upsert_key: None,
1000 importance: None,
1001 source: Some("agent".to_string()),
1002 lane: None,
1003 parallel: false,
1004 idempotency_key: None,
1005 wait: true,
1006 timeout_ms: None,
1007 poll_interval_ms: 300,
1008 occurrence_time: None,
1009 }
1010 }
1011}
1012
1013#[derive(Clone, Debug, Default)]
1018pub struct LessonMeta {
1019 pub lesson_type: Option<String>,
1020 pub lesson_scope: Option<String>,
1021 pub lesson_importance: Option<String>,
1022 pub lesson_conditions: Vec<String>,
1023}
1024
1025#[derive(Clone, Debug, Default)]
1028pub struct SessionScope {
1029 pub run_id: Option<String>,
1030 pub agent_id: Option<String>,
1031 pub user_id: Option<String>,
1032}
1033
1034pub struct RememberBuilder {
1050 options: RememberOptions,
1051}
1052
1053impl RememberBuilder {
1054 pub fn new(content: impl Into<String>) -> Self {
1055 Self {
1056 options: RememberOptions::new(content),
1057 }
1058 }
1059
1060 pub fn lesson(mut self, meta: LessonMeta) -> Self {
1061 self.options.lesson_type = meta.lesson_type;
1062 self.options.lesson_scope = meta.lesson_scope;
1063 self.options.lesson_importance = meta.lesson_importance;
1064 self.options.lesson_conditions = meta.lesson_conditions;
1065 self
1066 }
1067
1068 pub fn session(mut self, scope: SessionScope) -> Self {
1069 self.options.run_id = scope.run_id;
1070 self.options.agent_id = scope.agent_id.or(self.options.agent_id);
1071 self.options.user_id = scope.user_id;
1072 self
1073 }
1074
1075 pub fn metadata(mut self, metadata: Value) -> Self {
1076 self.options.metadata = Some(metadata);
1077 self
1078 }
1079
1080 pub fn hints(mut self, hints: Value) -> Self {
1081 self.options.hints = Some(hints);
1082 self
1083 }
1084
1085 pub fn payload(mut self, payload: Value) -> Self {
1086 self.options.payload = Some(payload);
1087 self
1088 }
1089
1090 pub fn upsert(mut self, key: impl Into<String>) -> Self {
1091 self.options.upsert_key = Some(key.into());
1092 self
1093 }
1094
1095 pub fn intent(mut self, intent: impl Into<String>) -> Self {
1096 self.options.intent = Some(intent.into());
1097 self
1098 }
1099
1100 pub fn lane(mut self, lane: impl Into<String>) -> Self {
1101 self.options.lane = Some(lane.into());
1102 self
1103 }
1104
1105 pub fn source(mut self, source: impl Into<String>) -> Self {
1106 self.options.source = Some(source.into());
1107 self
1108 }
1109
1110 pub fn importance(mut self, importance: impl Into<String>) -> Self {
1111 self.options.importance = Some(importance.into());
1112 self
1113 }
1114
1115 pub fn occurrence_time(mut self, ts_seconds: i64) -> Self {
1116 self.options.occurrence_time = Some(ts_seconds);
1117 self
1118 }
1119
1120 pub fn idempotency_key(mut self, key: impl Into<String>) -> Self {
1121 self.options.idempotency_key = Some(key.into());
1122 self
1123 }
1124
1125 pub fn wait(mut self, wait: bool) -> Self {
1126 self.options.wait = wait;
1127 self
1128 }
1129
1130 pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
1131 self.options.timeout_ms = Some(timeout_ms);
1132 self
1133 }
1134
1135 pub fn build(self) -> RememberOptions {
1138 self.options
1139 }
1140}
1141
1142#[derive(Clone, Debug)]
1143pub struct RecallOptions {
1144 pub run_id: Option<String>,
1145 pub query: String,
1146 pub schema: Option<String>,
1147 pub mode: String,
1148 pub direct_lane: String,
1149 pub include_linked_runs: bool,
1150 pub limit: u64,
1151 pub embedding: Vec<f32>,
1152 pub entry_types: Vec<String>,
1153 pub include_working_memory: bool,
1154 pub user_id: Option<String>,
1155 pub agent_id: Option<String>,
1156 pub lane: Option<String>,
1157 pub min_timestamp: Option<i64>,
1159 pub max_timestamp: Option<i64>,
1161 pub budget: Option<String>,
1163 pub rank_by: Option<String>,
1165 pub explain: Option<bool>,
1167 pub prefer_current_run: Option<bool>,
1172}
1173
1174impl RecallOptions {
1175 pub fn new(query: impl Into<String>) -> Self {
1176 Self {
1177 run_id: None,
1178 query: query.into(),
1179 schema: None,
1180 mode: "AGENT_ROUTED".to_string(),
1187 direct_lane: "SEMANTIC_SEARCH".to_string(),
1188 include_linked_runs: false,
1189 limit: 5,
1190 embedding: Vec::new(),
1191 entry_types: Vec::new(),
1192 include_working_memory: true,
1193 user_id: None,
1194 agent_id: None,
1195 lane: None,
1196 min_timestamp: None,
1197 max_timestamp: None,
1198 budget: None,
1199 rank_by: None,
1200 explain: None,
1201 prefer_current_run: None,
1202 }
1203 }
1204}
1205
1206#[derive(Clone, Debug)]
1207pub struct GetContextOptions {
1208 pub run_id: Option<String>,
1209 pub query: Option<String>,
1210 pub user_id: Option<String>,
1211 pub entry_types: Vec<String>,
1212 pub include_working_memory: bool,
1213 pub format: Option<String>,
1214 pub limit: Option<u64>,
1215 pub max_token_budget: Option<u32>,
1216 pub agent_id: Option<String>,
1217 pub mode: Option<String>,
1218 pub sections: Vec<String>,
1219 pub lane: Option<String>,
1220}
1221
1222impl Default for GetContextOptions {
1223 fn default() -> Self {
1224 Self {
1225 run_id: None,
1226 query: None,
1227 user_id: None,
1228 entry_types: Vec::new(),
1229 include_working_memory: true,
1230 format: None,
1231 limit: None,
1232 max_token_budget: None,
1233 agent_id: None,
1234 mode: None,
1235 sections: Vec::new(),
1236 lane: None,
1237 }
1238 }
1239}
1240
1241#[derive(Clone, Debug)]
1242pub struct ArchiveOptions {
1243 pub run_id: Option<String>,
1244 pub content: String,
1245 pub artifact_kind: String,
1246 pub metadata: Option<Value>,
1247 pub user_id: Option<String>,
1248 pub agent_id: Option<String>,
1249 pub origin_agent_id: Option<String>,
1250 pub source_attempt_id: Option<String>,
1251 pub source_tool: Option<String>,
1252 pub labels: Vec<String>,
1253 pub family: Option<String>,
1254 pub importance: Option<String>,
1255}
1256
1257impl ArchiveOptions {
1258 pub fn new(content: impl Into<String>, artifact_kind: impl Into<String>) -> Self {
1259 Self {
1260 run_id: None,
1261 content: content.into(),
1262 artifact_kind: artifact_kind.into(),
1263 metadata: None,
1264 user_id: None,
1265 agent_id: None,
1266 origin_agent_id: None,
1267 source_attempt_id: None,
1268 source_tool: None,
1269 labels: Vec::new(),
1270 family: None,
1271 importance: None,
1272 }
1273 }
1274}
1275
1276#[derive(Clone, Debug, Default)]
1280pub struct ArtifactProvenance {
1281 pub origin_agent_id: Option<String>,
1282 pub source_attempt_id: Option<String>,
1283 pub source_tool: Option<String>,
1284 pub labels: Vec<String>,
1285 pub family: Option<String>,
1286}
1287
1288pub struct ArchiveBuilder {
1304 options: ArchiveOptions,
1305}
1306
1307impl ArchiveBuilder {
1308 pub fn new(content: impl Into<String>, artifact_kind: impl Into<String>) -> Self {
1309 Self {
1310 options: ArchiveOptions::new(content, artifact_kind),
1311 }
1312 }
1313
1314 pub fn session(mut self, scope: SessionScope) -> Self {
1315 self.options.run_id = scope.run_id;
1316 self.options.agent_id = scope.agent_id;
1317 self.options.user_id = scope.user_id;
1318 self
1319 }
1320
1321 pub fn provenance(mut self, prov: ArtifactProvenance) -> Self {
1322 self.options.origin_agent_id = prov.origin_agent_id;
1323 self.options.source_attempt_id = prov.source_attempt_id;
1324 self.options.source_tool = prov.source_tool;
1325 self.options.labels = prov.labels;
1326 self.options.family = prov.family;
1327 self
1328 }
1329
1330 pub fn metadata(mut self, metadata: Value) -> Self {
1331 self.options.metadata = Some(metadata);
1332 self
1333 }
1334
1335 pub fn importance(mut self, importance: impl Into<String>) -> Self {
1336 self.options.importance = Some(importance.into());
1337 self
1338 }
1339
1340 pub fn label(mut self, label: impl Into<String>) -> Self {
1341 self.options.labels.push(label.into());
1342 self
1343 }
1344
1345 pub fn build(self) -> ArchiveOptions {
1346 self.options
1347 }
1348}
1349
1350#[derive(Clone, Debug)]
1351pub struct DereferenceOptions {
1352 pub run_id: Option<String>,
1353 pub reference_id: String,
1354 pub user_id: Option<String>,
1355 pub agent_id: Option<String>,
1356}
1357
1358impl DereferenceOptions {
1359 pub fn new(reference_id: impl Into<String>) -> Self {
1360 Self {
1361 run_id: None,
1362 reference_id: reference_id.into(),
1363 user_id: None,
1364 agent_id: None,
1365 }
1366 }
1367}
1368
1369#[derive(Clone, Debug)]
1370pub struct MemoryHealthOptions {
1371 pub run_id: Option<String>,
1372 pub user_id: Option<String>,
1373 pub stale_threshold_days: u32,
1374 pub limit: u32,
1375}
1376
1377impl Default for MemoryHealthOptions {
1378 fn default() -> Self {
1379 Self {
1380 run_id: None,
1381 user_id: None,
1382 stale_threshold_days: 30,
1383 limit: 500,
1384 }
1385 }
1386}
1387
1388#[derive(Clone, Debug)]
1389pub struct DiagnoseOptions {
1390 pub run_id: Option<String>,
1391 pub error_text: String,
1392 pub error_type: Option<String>,
1393 pub limit: u64,
1394 pub user_id: Option<String>,
1395}
1396
1397impl DiagnoseOptions {
1398 pub fn new(error_text: impl Into<String>) -> Self {
1399 Self {
1400 run_id: None,
1401 error_text: error_text.into(),
1402 error_type: None,
1403 limit: 10,
1404 user_id: None,
1405 }
1406 }
1407}
1408
1409#[derive(Clone, Debug, Default)]
1410pub struct ReflectOptions {
1411 pub run_id: Option<String>,
1412 pub include_linked_runs: bool,
1413 pub user_id: Option<String>,
1414 pub step_id: Option<String>,
1415 pub checkpoint_id: Option<String>,
1416 pub last_n_items: Option<u64>,
1417 pub include_step_outcomes: Option<bool>,
1418}
1419
1420#[derive(Clone, Debug, Default)]
1421pub struct ForgetOptions {
1422 pub run_id: Option<String>,
1423 pub lesson_id: Option<String>,
1424}
1425
1426impl ForgetOptions {
1427 pub fn for_run(run_id: impl Into<String>) -> Self {
1428 Self {
1429 run_id: Some(run_id.into()),
1430 lesson_id: None,
1431 }
1432 }
1433
1434 pub fn for_lesson(lesson_id: impl Into<String>) -> Self {
1435 Self {
1436 run_id: None,
1437 lesson_id: Some(lesson_id.into()),
1438 }
1439 }
1440}
1441
1442#[derive(Clone, Debug)]
1443pub struct CheckpointOptions {
1444 pub run_id: Option<String>,
1445 pub label: Option<String>,
1446 pub context_snapshot: String,
1447 pub metadata: Option<Value>,
1448 pub user_id: Option<String>,
1449 pub agent_id: Option<String>,
1450}
1451
1452impl CheckpointOptions {
1453 pub fn new(context_snapshot: impl Into<String>) -> Self {
1454 Self {
1455 run_id: None,
1456 label: None,
1457 context_snapshot: context_snapshot.into(),
1458 metadata: None,
1459 user_id: None,
1460 agent_id: None,
1461 }
1462 }
1463}
1464
1465#[derive(Clone, Debug)]
1466pub struct RegisterAgentOptions {
1467 pub run_id: Option<String>,
1468 pub agent_id: String,
1469 pub role: String,
1470 pub capabilities: Vec<String>,
1471 pub status: String,
1472 pub read_scopes: Vec<String>,
1473 pub write_scopes: Vec<String>,
1474 pub shared_memory_lanes: Vec<String>,
1475}
1476
1477impl RegisterAgentOptions {
1478 pub fn new(agent_id: impl Into<String>) -> Self {
1479 Self {
1480 run_id: None,
1481 agent_id: agent_id.into(),
1482 role: String::new(),
1483 capabilities: Vec::new(),
1484 status: "active".to_string(),
1485 read_scopes: Vec::new(),
1486 write_scopes: Vec::new(),
1487 shared_memory_lanes: Vec::new(),
1488 }
1489 }
1490}
1491
1492#[derive(Clone, Debug, Default)]
1493pub struct ListAgentsOptions {
1494 pub run_id: Option<String>,
1495}
1496
1497#[derive(Clone, Debug)]
1498pub struct RecordOutcomeOptions {
1499 pub run_id: Option<String>,
1500 pub reference_id: String,
1501 pub outcome: String,
1502 pub signal: f32,
1503 pub rationale: String,
1504 pub agent_id: Option<String>,
1505 pub user_id: Option<String>,
1506}
1507
1508impl RecordOutcomeOptions {
1509 pub fn new(reference_id: impl Into<String>, outcome: impl Into<String>) -> Self {
1510 Self {
1511 run_id: None,
1512 reference_id: reference_id.into(),
1513 outcome: outcome.into(),
1514 signal: 0.0,
1515 rationale: String::new(),
1516 agent_id: None,
1517 user_id: None,
1518 }
1519 }
1520}
1521
1522#[derive(Clone, Debug, Default)]
1528pub struct OptimizePromptOptions {
1529 pub agent_id: String,
1530 pub auto_activate: bool,
1535 pub run_id: Option<String>,
1536 pub project_id: Option<String>,
1539}
1540
1541impl OptimizePromptOptions {
1542 pub fn new(agent_id: impl Into<String>) -> Self {
1543 Self {
1544 agent_id: agent_id.into(),
1545 auto_activate: false,
1546 run_id: None,
1547 project_id: None,
1548 }
1549 }
1550}
1551
1552#[derive(Clone, Debug, Default)]
1554pub struct OptimizeSkillOptions {
1555 pub skill_id: String,
1556 pub auto_activate: bool,
1557 pub project_id: Option<String>,
1558}
1559
1560impl OptimizeSkillOptions {
1561 pub fn new(skill_id: impl Into<String>) -> Self {
1562 Self {
1563 skill_id: skill_id.into(),
1564 auto_activate: false,
1565 project_id: None,
1566 }
1567 }
1568}
1569
1570#[derive(Clone, Debug, Default)]
1575pub struct CircuitBreakOptions {
1576 pub run_id: Option<String>,
1577 pub reason: Option<String>,
1578 pub agent_id: Option<String>,
1579}
1580
1581impl CircuitBreakOptions {
1582 pub fn new() -> Self {
1583 Self::default()
1584 }
1585 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
1586 self.reason = Some(reason.into());
1587 self
1588 }
1589}
1590
1591#[derive(Clone, Debug)]
1592pub struct RecordStepOutcomeOptions {
1593 pub run_id: Option<String>,
1594 pub step_id: String,
1595 pub step_name: Option<String>,
1596 pub outcome: String,
1597 pub signal: f32,
1598 pub rationale: String,
1599 pub directive_hint: Option<String>,
1600 pub agent_id: Option<String>,
1601 pub user_id: Option<String>,
1602 pub metadata: Option<Value>,
1603}
1604
1605impl RecordStepOutcomeOptions {
1606 pub fn new(step_id: impl Into<String>, outcome: impl Into<String>) -> Self {
1607 Self {
1608 run_id: None,
1609 step_id: step_id.into(),
1610 step_name: None,
1611 outcome: outcome.into(),
1612 signal: 0.0,
1613 rationale: String::new(),
1614 directive_hint: None,
1615 agent_id: None,
1616 user_id: None,
1617 metadata: None,
1618 }
1619 }
1620}
1621
1622#[derive(Clone, Debug)]
1623pub struct SurfaceStrategiesOptions {
1624 pub run_id: Option<String>,
1625 pub lesson_types: Vec<String>,
1626 pub max_strategies: u32,
1627 pub user_id: Option<String>,
1628}
1629
1630impl Default for SurfaceStrategiesOptions {
1631 fn default() -> Self {
1632 Self {
1633 run_id: None,
1634 lesson_types: Vec::new(),
1635 max_strategies: 5,
1636 user_id: None,
1637 }
1638 }
1639}
1640
1641#[derive(Clone, Debug)]
1642pub struct HandoffOptions {
1643 pub run_id: Option<String>,
1644 pub task_id: String,
1645 pub from_agent_id: String,
1646 pub to_agent_id: String,
1647 pub content: String,
1648 pub requested_action: String,
1649 pub metadata: Option<Value>,
1650 pub user_id: Option<String>,
1651}
1652
1653impl HandoffOptions {
1654 pub fn new(
1655 task_id: impl Into<String>,
1656 from_agent_id: impl Into<String>,
1657 to_agent_id: impl Into<String>,
1658 content: impl Into<String>,
1659 ) -> Self {
1660 Self {
1661 run_id: None,
1662 task_id: task_id.into(),
1663 from_agent_id: from_agent_id.into(),
1664 to_agent_id: to_agent_id.into(),
1665 content: content.into(),
1666 requested_action: "continue".to_string(),
1667 metadata: None,
1668 user_id: None,
1669 }
1670 }
1671}
1672
1673#[derive(Clone, Debug)]
1674pub struct FeedbackOptions {
1675 pub run_id: Option<String>,
1676 pub handoff_id: String,
1677 pub verdict: String,
1678 pub comments: String,
1679 pub from_agent_id: Option<String>,
1680 pub metadata: Option<Value>,
1681 pub user_id: Option<String>,
1682}
1683
1684impl FeedbackOptions {
1685 pub fn new(handoff_id: impl Into<String>, verdict: impl Into<String>) -> Self {
1686 Self {
1687 run_id: None,
1688 handoff_id: handoff_id.into(),
1689 verdict: verdict.into(),
1690 comments: String::new(),
1691 from_agent_id: None,
1692 metadata: None,
1693 user_id: None,
1694 }
1695 }
1696}
1697
1698impl Client {
1699 pub async fn remember(&self, options: RememberOptions) -> Result<Value> {
1700 let run_id = resolve_helper_run_id(options.run_id, self.transport.run_id(), "remember")?;
1701 let content = require_non_empty_string(options.content, "content")?;
1702 let item_id = options
1703 .item_id
1704 .unwrap_or_else(|| generate_helper_id("remember"));
1705 let accepted = self
1706 .control
1707 .ingest(prune_nulls(json!({
1708 "run_id": run_id,
1709 "agent_id": options.agent_id.unwrap_or_else(|| "sdk-client".to_string()),
1710 "idempotency_key": options.idempotency_key.unwrap_or_else(|| item_id.clone()),
1711 "parallel": options.parallel,
1712 "items": [{
1713 "item_id": item_id,
1714 "content_type": options.content_type,
1715 "text": content,
1716 "payload_json": encode_optional_json(options.payload.as_ref())?,
1717 "hints_json": encode_optional_json(options.hints.as_ref())?,
1718 "metadata_json": encode_optional_json(options.metadata.as_ref())?,
1719 "intent": options.intent,
1720 "lesson_type": options.lesson_type,
1721 "lesson_scope": options.lesson_scope,
1722 "lesson_importance": options.lesson_importance,
1723 "lesson_conditions_json": encode_string_vec(&options.lesson_conditions)?,
1724 "user_id": options.user_id,
1725 "upsert_key": options.upsert_key,
1726 "importance": options.importance,
1727 "source": options.source.unwrap_or_else(|| "agent".to_string()),
1728 "lane": options.lane,
1729 "occurrence_time": options.occurrence_time.unwrap_or(0),
1730 }],
1731 })))
1732 .await?;
1733
1734 if !options.wait {
1735 return Ok(accepted);
1736 }
1737
1738 let Some(job_id) = accepted.get("job_id").and_then(|value| value.as_str()) else {
1739 return Ok(accepted);
1740 };
1741
1742 self.wait_for_ingest_job(
1743 &run_id,
1744 job_id,
1745 options
1746 .timeout_ms
1747 .unwrap_or_else(|| self.transport.timeout.as_millis() as u64),
1748 options.poll_interval_ms,
1749 )
1750 .await
1751 }
1752
1753 pub async fn recall(&self, options: RecallOptions) -> Result<Value> {
1754 let run_id = resolve_helper_run_id(options.run_id, self.transport.run_id(), "recall")?;
1755 self.control
1756 .query(prune_nulls(json!({
1757 "run_id": run_id,
1758 "query": require_non_empty_string(options.query, "query")?,
1759 "schema": options.schema,
1760 "mode": options.mode,
1761 "direct_lane": options.direct_lane,
1762 "include_linked_runs": options.include_linked_runs,
1763 "limit": options.limit,
1764 "embedding": options.embedding,
1765 "entry_types": if options.entry_types.is_empty() { Value::Null } else { json!(options.entry_types) },
1766 "include_working_memory": options.include_working_memory,
1767 "user_id": options.user_id,
1768 "agent_id": options.agent_id,
1769 "lane": options.lane,
1770 "min_timestamp": options.min_timestamp.unwrap_or(0),
1771 "max_timestamp": options.max_timestamp.unwrap_or(0),
1772 "budget": options.budget,
1773 "rank_by": options.rank_by,
1774 "explain": options.explain,
1775 "prefer_current_run": options.prefer_current_run,
1776 })))
1777 .await
1778 }
1779
1780 pub async fn get_context(&self, options: GetContextOptions) -> Result<Value> {
1781 let run_id = resolve_helper_run_id(options.run_id, self.transport.run_id(), "get_context")?;
1782 self.control
1783 .context(prune_nulls(json!({
1784 "run_id": run_id,
1785 "query": require_non_empty_string(options.query.unwrap_or_default(), "query")?,
1786 "user_id": options.user_id,
1787 "entry_types": if options.entry_types.is_empty() { Value::Null } else { json!(options.entry_types) },
1788 "include_working_memory": options.include_working_memory,
1789 "format": options.format.unwrap_or_else(|| "structured".to_string()),
1790 "limit": options.limit.unwrap_or(5),
1791 "max_token_budget": options.max_token_budget.unwrap_or(0),
1792 "agent_id": options.agent_id,
1793 "mode": options.mode.unwrap_or_else(|| "full".to_string()),
1794 "sections": if options.sections.is_empty() { Value::Null } else { json!(options.sections) },
1795 "lane": options.lane,
1796 })))
1797 .await
1798 }
1799
1800 pub async fn archive(&self, options: ArchiveOptions) -> Result<Value> {
1801 let run_id = resolve_helper_run_id(options.run_id, self.transport.run_id(), "archive")?;
1802 let content = require_non_empty_string(options.content, "content")?;
1803 let artifact_kind = require_non_empty_string(options.artifact_kind, "artifact_kind")?;
1804 let agent_id = options.agent_id.clone();
1805 self.control
1806 .archive_block(prune_nulls(json!({
1807 "run_id": run_id,
1808 "content": content,
1809 "artifact_kind": artifact_kind,
1810 "metadata_json": encode_optional_json(options.metadata.as_ref())?,
1811 "user_id": options.user_id,
1812 "agent_id": agent_id.clone(),
1813 "origin_agent_id": options.origin_agent_id.or(agent_id),
1814 "source_attempt_id": options.source_attempt_id,
1815 "source_tool": options.source_tool,
1816 "labels": if options.labels.is_empty() { Value::Null } else { json!(options.labels) },
1817 "family": options.family,
1818 "importance": options.importance,
1819 })))
1820 .await
1821 }
1822
1823 pub async fn archive_block(&self, options: ArchiveOptions) -> Result<Value> {
1824 self.archive(options).await
1825 }
1826
1827 pub async fn dereference(&self, options: DereferenceOptions) -> Result<Value> {
1828 let run_id =
1829 resolve_helper_run_id(options.run_id, self.transport.run_id(), "dereference")?;
1830 self.control
1831 .dereference(prune_nulls(json!({
1832 "run_id": run_id,
1833 "reference_id": require_non_empty_string(options.reference_id, "reference_id")?,
1834 "user_id": options.user_id,
1835 "agent_id": options.agent_id,
1836 })))
1837 .await
1838 }
1839
1840 pub async fn memory_health(&self, options: MemoryHealthOptions) -> Result<Value> {
1841 let run_id =
1842 resolve_helper_run_id(options.run_id, self.transport.run_id(), "memory_health")?;
1843 self.control
1844 .memory_health(prune_nulls(json!({
1845 "run_id": run_id,
1846 "user_id": options.user_id,
1847 "stale_threshold_days": options.stale_threshold_days,
1848 "limit": options.limit,
1849 })))
1850 .await
1851 }
1852
1853 pub async fn diagnose(&self, options: DiagnoseOptions) -> Result<Value> {
1854 let run_id = resolve_helper_run_id(options.run_id, self.transport.run_id(), "diagnose")?;
1855 self.control
1856 .diagnose(prune_nulls(json!({
1857 "run_id": run_id,
1858 "error_text": require_non_empty_string(options.error_text, "error_text")?,
1859 "error_type": options.error_type,
1860 "limit": options.limit,
1861 "user_id": options.user_id,
1862 })))
1863 .await
1864 }
1865
1866 pub async fn reflect(&self, options: ReflectOptions) -> Result<Value> {
1867 let run_id = resolve_helper_run_id(options.run_id, self.transport.run_id(), "reflect")?;
1868 self.control
1869 .reflect(prune_nulls(json!({
1870 "run_id": run_id,
1871 "include_linked_runs": options.include_linked_runs,
1872 "user_id": options.user_id,
1873 "step_id": options.step_id,
1874 "checkpoint_id": options.checkpoint_id,
1875 "last_n_items": options.last_n_items,
1876 "include_step_outcomes": options.include_step_outcomes,
1877 })))
1878 .await
1879 }
1880
1881 pub async fn forget(&self, options: ForgetOptions) -> Result<Value> {
1882 let delete_lesson = options
1883 .lesson_id
1884 .as_ref()
1885 .map(|value| !value.trim().is_empty())
1886 .unwrap_or(false);
1887 let run_id = if options.run_id.is_some() {
1888 options.run_id
1889 } else if delete_lesson {
1890 None
1891 } else {
1892 self.transport.run_id()
1893 };
1894 let delete_run = run_id
1895 .as_ref()
1896 .map(|value| !value.trim().is_empty())
1897 .unwrap_or(false);
1898
1899 if (delete_lesson as u8) + (delete_run as u8) != 1 {
1900 return Err(SdkError::ValidationError(
1901 "forget requires either lesson_id or run_id, but not both".to_string(),
1902 ));
1903 }
1904
1905 if delete_lesson {
1906 return self
1907 .control
1908 .delete_lesson(json!({ "lesson_id": options.lesson_id.unwrap_or_default() }))
1909 .await;
1910 }
1911
1912 self.control
1913 .delete_run(json!({ "run_id": run_id.unwrap_or_default() }))
1914 .await
1915 }
1916
1917 pub async fn checkpoint(&self, options: CheckpointOptions) -> Result<Value> {
1918 let run_id = resolve_helper_run_id(options.run_id, self.transport.run_id(), "checkpoint")?;
1919 self.control
1920 .checkpoint(prune_nulls(json!({
1921 "run_id": run_id,
1922 "label": options.label,
1923 "context_snapshot": require_non_empty_string(options.context_snapshot, "context_snapshot")?,
1924 "metadata_json": encode_optional_json(options.metadata.as_ref())?,
1925 "user_id": options.user_id,
1926 "agent_id": options.agent_id,
1927 })))
1928 .await
1929 }
1930
1931 pub async fn register_agent(&self, options: RegisterAgentOptions) -> Result<Value> {
1932 let run_id =
1933 resolve_helper_run_id(options.run_id, self.transport.run_id(), "register_agent")?;
1934 self.control
1935 .register_agent(prune_nulls(json!({
1936 "run_id": run_id,
1937 "agent_id": require_non_empty_string(options.agent_id, "agent_id")?,
1938 "role": options.role,
1939 "capabilities": if options.capabilities.is_empty() { Value::Null } else { json!(options.capabilities) },
1940 "status": options.status,
1941 "read_scopes": if options.read_scopes.is_empty() { Value::Null } else { json!(options.read_scopes) },
1942 "write_scopes": if options.write_scopes.is_empty() { Value::Null } else { json!(options.write_scopes) },
1943 "shared_memory_lanes": if options.shared_memory_lanes.is_empty() { Value::Null } else { json!(options.shared_memory_lanes) },
1944 })))
1945 .await
1946 }
1947
1948 pub async fn list_agents(&self, options: ListAgentsOptions) -> Result<Value> {
1949 let run_id = resolve_helper_run_id(options.run_id, self.transport.run_id(), "list_agents")?;
1950 self.control.list_agents(json!({ "run_id": run_id })).await
1951 }
1952
1953 pub async fn record_outcome(&self, options: RecordOutcomeOptions) -> Result<Value> {
1954 let run_id =
1955 resolve_helper_run_id(options.run_id, self.transport.run_id(), "record_outcome")?;
1956 self.control
1957 .record_outcome(prune_nulls(json!({
1958 "run_id": run_id,
1959 "reference_id": require_non_empty_string(options.reference_id, "reference_id")?,
1960 "outcome": require_non_empty_string(options.outcome, "outcome")?,
1961 "signal": options.signal,
1962 "rationale": options.rationale,
1963 "agent_id": options.agent_id,
1964 "user_id": options.user_id,
1965 })))
1966 .await
1967 }
1968
1969 pub async fn optimize_prompt(&self, options: OptimizePromptOptions) -> Result<Value> {
1975 let run_id = options
1976 .run_id
1977 .clone()
1978 .or_else(|| self.transport.run_id())
1979 .unwrap_or_default();
1980 self.control
1981 .optimize_prompt(prune_nulls(json!({
1982 "agent_id": require_non_empty_string(options.agent_id, "agent_id")?,
1983 "auto_activate": options.auto_activate,
1984 "run_id": if run_id.is_empty() { None } else { Some(run_id) },
1985 "project_id": options.project_id,
1986 })))
1987 .await
1988 }
1989
1990 pub async fn optimize_skill(&self, options: OptimizeSkillOptions) -> Result<Value> {
1992 self.control
1993 .optimize_skill(prune_nulls(json!({
1994 "skill_id": require_non_empty_string(options.skill_id, "skill_id")?,
1995 "auto_activate": options.auto_activate,
1996 "project_id": options.project_id,
1997 })))
1998 .await
1999 }
2000
2001 pub async fn circuit_break(&self, options: CircuitBreakOptions) -> Result<Value> {
2007 let run_id =
2008 resolve_helper_run_id(options.run_id, self.transport.run_id(), "circuit_break")?;
2009 self.control
2010 .circuit_break(prune_nulls(json!({
2011 "run_id": run_id,
2012 "reason": options.reason,
2013 "agent_id": options.agent_id,
2014 })))
2015 .await
2016 }
2017
2018 pub async fn record_step_outcome(&self, options: RecordStepOutcomeOptions) -> Result<Value> {
2019 let run_id =
2020 resolve_helper_run_id(options.run_id, self.transport.run_id(), "record_step_outcome")?;
2021 self.control
2022 .record_outcome(prune_nulls(json!({
2023 "run_id": run_id,
2024 "step_id": require_non_empty_string(options.step_id, "step_id")?,
2025 "step_name": options.step_name,
2026 "outcome": require_non_empty_string(options.outcome, "outcome")?,
2027 "signal": options.signal,
2028 "rationale": options.rationale,
2029 "directive_hint": options.directive_hint,
2030 "agent_id": options.agent_id,
2031 "user_id": options.user_id,
2032 "metadata_json": encode_optional_json(options.metadata.as_ref())?,
2033 })))
2034 .await
2035 }
2036
2037 pub async fn surface_strategies(&self, options: SurfaceStrategiesOptions) -> Result<Value> {
2038 let run_id = resolve_helper_run_id(
2039 options.run_id,
2040 self.transport.run_id(),
2041 "surface_strategies",
2042 )?;
2043 self.control
2044 .surface_strategies(prune_nulls(json!({
2045 "run_id": run_id,
2046 "lesson_types": if options.lesson_types.is_empty() { Value::Null } else { json!(options.lesson_types) },
2047 "max_strategies": options.max_strategies,
2048 "user_id": options.user_id,
2049 })))
2050 .await
2051 }
2052
2053 pub async fn handoff(&self, options: HandoffOptions) -> Result<Value> {
2054 let run_id = resolve_helper_run_id(options.run_id, self.transport.run_id(), "handoff")?;
2055 self.control
2056 .create_handoff(prune_nulls(json!({
2057 "run_id": run_id,
2058 "task_id": require_non_empty_string(options.task_id, "task_id")?,
2059 "from_agent_id": require_non_empty_string(options.from_agent_id, "from_agent_id")?,
2060 "to_agent_id": require_non_empty_string(options.to_agent_id, "to_agent_id")?,
2061 "content": require_non_empty_string(options.content, "content")?,
2062 "requested_action": options.requested_action,
2063 "metadata_json": encode_optional_json(options.metadata.as_ref())?,
2064 "user_id": options.user_id,
2065 })))
2066 .await
2067 }
2068
2069 pub async fn feedback(&self, options: FeedbackOptions) -> Result<Value> {
2070 let run_id = resolve_helper_run_id(options.run_id, self.transport.run_id(), "feedback")?;
2071 self.control
2072 .submit_feedback(prune_nulls(json!({
2073 "run_id": run_id,
2074 "handoff_id": require_non_empty_string(options.handoff_id, "handoff_id")?,
2075 "verdict": require_non_empty_string(options.verdict, "verdict")?,
2076 "comments": options.comments,
2077 "from_agent_id": options.from_agent_id,
2078 "metadata_json": encode_optional_json(options.metadata.as_ref())?,
2079 "user_id": options.user_id,
2080 })))
2081 .await
2082 }
2083
2084 async fn wait_for_ingest_job(
2085 &self,
2086 run_id: &str,
2087 job_id: &str,
2088 timeout_ms: u64,
2089 poll_interval_ms: u64,
2090 ) -> Result<Value> {
2091 let deadline = Instant::now() + Duration::from_millis(timeout_ms);
2092 loop {
2093 let job = self
2094 .control
2095 .get_ingest_job(json!({ "run_id": run_id, "job_id": job_id }))
2096 .await?;
2097 if job
2098 .get("done")
2099 .and_then(|value| value.as_bool())
2100 .unwrap_or(false)
2101 {
2102 return Ok(job);
2103 }
2104 if Instant::now() >= deadline {
2105 return Err(SdkError::TransportError {
2106 kind: TransportFailureKind::DeadlineExceeded,
2107 message: format!("timed out waiting for ingest job {}", job_id),
2108 });
2109 }
2110 sleep(Duration::from_millis(poll_interval_ms)).await;
2111 }
2112 }
2113}
2114
2115#[derive(Clone)]
2116pub struct AuthClient {
2117 transport: Arc<TransportEngine>,
2118}
2119
2120impl AuthClient {
2121 pub async fn health(&self) -> Result<Value> {
2122 self.transport.invoke("auth.health", json!({})).await
2123 }
2124
2125 pub fn set_api_key(&self, api_key: Option<String>) {
2126 self.transport.set_api_key(api_key);
2127 }
2128
2129 pub fn set_token(&self, token: Option<String>) {
2130 self.set_api_key(token);
2131 }
2132
2133 pub fn set_run_id(&self, run_id: Option<String>) {
2134 self.transport.set_run_id(run_id);
2135 }
2136}
2137
2138macro_rules! define_auth_payload_methods {
2139 ($($name:ident => $op_key:literal),+ $(,)?) => {
2140 impl AuthClient {
2141 $(
2142 pub async fn $name<T: Serialize>(&self, payload: T) -> Result<Value> {
2143 self.transport.invoke_serialized($op_key, payload).await
2144 }
2145 )+
2146 }
2147 };
2148}
2149
2150define_auth_payload_methods!(
2151 create_user => "auth.create_user",
2152 rotate_user_api_key => "auth.rotate_user_api_key",
2153 revoke_user_api_key => "auth.revoke_user_api_key",
2154 list_users => "auth.list_users",
2155 get_user => "auth.get_user",
2156 delete_user => "auth.delete_user"
2157);
2158
2159#[derive(Clone)]
2160pub struct CoreClient {
2161 transport: Arc<TransportEngine>,
2162}
2163
2164macro_rules! define_core_payload_methods {
2165 ($($name:ident => $op_key:literal),+ $(,)?) => {
2166 impl CoreClient {
2167 $(
2168 pub async fn $name<T: Serialize>(&self, payload: T) -> Result<Value> {
2169 self.transport.invoke_serialized($op_key, payload).await
2170 }
2171 )+
2172 }
2173 };
2174}
2175
2176define_core_payload_methods!(
2177 insert => "core.insert",
2178 batch_insert => "core.batch_insert",
2179 search => "core.search",
2180 delete_node => "core.delete_node",
2181 delete_run => "core.delete_run",
2182 create_session => "core.create_session",
2183 snapshot_session => "core.snapshot_session",
2184 load_session => "core.load_session",
2185 commit_session => "core.commit_session",
2186 drop_session => "core.drop_session",
2187 write_memory => "core.write_memory",
2188 read_memory => "core.read_memory",
2189 add_memory => "core.add_memory",
2190 get_memory => "core.get_memory",
2191 clear_memory => "core.clear_memory",
2192 grant_permission => "core.grant_permission",
2193 revoke_permission => "core.revoke_permission",
2194 check_permission => "core.check_permission",
2195 unsubscribe_events => "core.unsubscribe_events"
2196);
2197
2198impl CoreClient {
2199 pub async fn subscribe_events<T: Serialize>(&self, payload: T) -> Result<ValueStream> {
2200 self.transport.invoke_stream_serialized("core.subscribe_events", payload).await
2201 }
2202
2203 pub async fn watch_memory<T: Serialize>(&self, payload: T) -> Result<ValueStream> {
2204 self.transport.invoke_stream_serialized("core.watch_memory", payload).await
2205 }
2206
2207 pub async fn list_subscriptions(&self) -> Result<Value> {
2208 self.transport
2209 .invoke("core.list_subscriptions", json!({}))
2210 .await
2211 }
2212
2213 pub async fn storage_stats(&self) -> Result<Value> {
2214 self.transport.invoke("core.storage_stats", json!({})).await
2215 }
2216
2217 pub async fn trigger_compaction(&self) -> Result<Value> {
2218 self.transport
2219 .invoke("core.trigger_compaction", json!({}))
2220 .await
2221 }
2222}
2223
2224#[derive(Clone)]
2225pub struct ControlClient {
2226 transport: Arc<TransportEngine>,
2227}
2228
2229macro_rules! define_control_payload_methods {
2230 ($($name:ident => $op_key:literal),+ $(,)?) => {
2231 impl ControlClient {
2232 $(
2233 pub async fn $name<T: Serialize>(&self, payload: T) -> Result<Value> {
2234 self.transport.invoke_serialized($op_key, payload).await
2235 }
2236 )+
2237 }
2238 };
2239}
2240
2241define_control_payload_methods!(
2242 register_agent => "control.register_agent",
2243 agent_heartbeat => "control.agent_heartbeat",
2244 context_snapshot => "control.context_snapshot",
2245 link_run => "control.link_run",
2246 unlink_run => "control.unlink_run",
2247 ingest => "control.ingest",
2248 batch_insert => "control.batch_insert",
2249 get_ingest_job => "control.get_ingest_job",
2250 get_run_ingest_stats => "control.get_run_ingest_stats",
2251 query => "control.query",
2252 diagnose => "control.diagnose",
2253 delete_run => "control.delete_run",
2254 reflect => "control.reflect",
2255 lessons => "control.lessons",
2256 delete_lesson => "control.delete_lesson",
2257 context => "control.context",
2258 archive_block => "control.archive_block",
2259 dereference => "control.dereference",
2260 memory_health => "control.memory_health",
2261 checkpoint => "control.checkpoint",
2262 list_agents => "control.list_agents",
2263 create_handoff => "control.create_handoff",
2264 submit_feedback => "control.submit_feedback",
2265 circuit_break => "control.circuit_break",
2266 record_outcome => "control.record_outcome",
2267 record_step_outcome => "control.record_step_outcome",
2268 surface_strategies => "control.surface_strategies",
2269 create_session => "control.create_session",
2270 get_session => "control.get_session",
2271 close_session => "control.close_session",
2272 set_prompt => "control.set_prompt",
2273 get_prompt => "control.get_prompt",
2274 list_prompt_versions => "control.list_prompt_versions",
2275 activate_prompt_version => "control.activate_prompt_version",
2276 optimize_prompt => "control.optimize_prompt",
2277 get_prompt_diff => "control.get_prompt_diff",
2278 create_project => "control.create_project",
2279 get_project => "control.get_project",
2280 list_projects => "control.list_projects",
2281 update_project => "control.update_project",
2282 delete_project => "control.delete_project",
2283 create_agent_definition => "control.create_agent_definition",
2284 get_agent_definition => "control.get_agent_definition",
2285 list_agent_definitions => "control.list_agent_definitions",
2286 update_agent_definition => "control.update_agent_definition",
2287 delete_agent_definition => "control.delete_agent_definition",
2288 list_run_history => "control.list_run_history",
2289 get_run_history => "control.get_run_history",
2290 create_skill => "control.create_skill",
2291 get_skill => "control.get_skill",
2292 list_skills => "control.list_skills",
2293 update_skill => "control.update_skill",
2294 delete_skill => "control.delete_skill",
2295 list_skill_versions => "control.list_skill_versions",
2296 activate_skill_version => "control.activate_skill_version",
2297 optimize_skill => "control.optimize_skill",
2298 get_skill_diff => "control.get_skill_diff",
2299);
2300
2301macro_rules! define_client_control_delegates {
2302 ($($name:ident),+ $(,)?) => {
2303 impl Client {
2304 $(
2305 pub async fn $name<T: Serialize>(&self, payload: T) -> Result<Value> {
2306 self.control.$name(payload).await
2307 }
2308 )+
2309 }
2310 };
2311}
2312
2313define_client_control_delegates!(
2314 agent_heartbeat, context_snapshot,
2315 link_run, unlink_run,
2316 ingest, batch_insert, get_ingest_job, get_run_ingest_stats,
2317 query,
2318 delete_run,
2319 lessons, delete_lesson,
2320 context,
2321 create_handoff, submit_feedback,
2322 create_session, get_session, close_session,
2323 set_prompt, get_prompt, list_prompt_versions,
2324 activate_prompt_version, get_prompt_diff,
2325 create_project, get_project, list_projects, update_project, delete_project,
2326 create_agent_definition, get_agent_definition, list_agent_definitions,
2327 update_agent_definition, delete_agent_definition,
2328 list_run_history, get_run_history,
2329 create_skill, get_skill, list_skills, update_skill, delete_skill,
2330 list_skill_versions, activate_skill_version, get_skill_diff,
2331);
2332
2333impl Client {
2334 pub async fn subscribe<T: Serialize>(&self, payload: T) -> Result<ValueStream> {
2335 self.control.subscribe(payload).await
2336 }
2337}
2338
2339impl ControlClient {
2340 pub async fn subscribe<T: Serialize>(&self, payload: T) -> Result<ValueStream> {
2341 self.transport.invoke_stream_serialized("control.subscribe", payload).await
2342 }
2343}
2344
2345fn parse_sse_byte_stream(
2346 byte_stream: impl Stream<Item = reqwest::Result<bytes::Bytes>> + Send + 'static,
2347) -> impl Stream<Item = Result<Value>> + Send {
2348 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Value>>(64);
2349 tokio::spawn(async move {
2350 tokio::pin!(byte_stream);
2351 let mut buffer = String::new();
2352 while let Some(chunk_result) = byte_stream.next().await {
2353 match chunk_result {
2354 Ok(chunk) => {
2355 buffer.push_str(&String::from_utf8_lossy(&chunk));
2356 while let Some(newline_pos) = buffer.find('\n') {
2357 let line = buffer[..newline_pos].trim().to_string();
2358 buffer = buffer[newline_pos + 1..].to_string();
2359 if line.is_empty() || line.starts_with(':') {
2360 continue;
2361 }
2362 let data = if line.starts_with("data: ") {
2363 &line[6..]
2364 } else {
2365 &line
2366 };
2367 let value = match serde_json::from_str::<Value>(data) {
2368 Ok(value) => Ok(value),
2369 Err(_) => Ok(Value::String(data.to_string())),
2370 };
2371 if tx.send(value).await.is_err() {
2372 return;
2373 }
2374 }
2375 }
2376 Err(e) => {
2377 let _ = tx
2378 .send(Err(SdkError::TransportError {
2379 kind: TransportFailureKind::Io,
2380 message: e.to_string(),
2381 }))
2382 .await;
2383 return;
2384 }
2385 }
2386 }
2387 let remaining = buffer.trim().to_string();
2389 if !remaining.is_empty() && !remaining.starts_with(':') {
2390 let data = if remaining.starts_with("data: ") {
2391 &remaining[6..]
2392 } else {
2393 &remaining
2394 };
2395 let value = match serde_json::from_str::<Value>(data) {
2396 Ok(value) => Ok(value),
2397 Err(_) => Ok(Value::String(data.to_string())),
2398 };
2399 let _ = tx.send(value).await;
2400 }
2401 });
2402 tokio_stream::wrappers::ReceiverStream::new(rx)
2403}
2404
2405fn hydrate_pubsub_event(value: &mut Value) {
2412 let Some(obj) = value.as_object_mut() else {
2413 return;
2414 };
2415
2416 if let Some(Value::String(raw)) = obj.remove("metadata_json") {
2417 let parsed = serde_json::from_str::<Value>(&raw).unwrap_or(Value::String(raw));
2418 obj.insert("metadata".to_string(), parsed);
2419 }
2420
2421 if let Some(Value::String(raw)) = obj.remove("entry_json") {
2422 let parsed = serde_json::from_str::<Value>(&raw).unwrap_or(Value::String(raw));
2423 obj.insert("entry".to_string(), parsed);
2424 }
2425
2426 let event_type = obj
2430 .get("type")
2431 .and_then(Value::as_str)
2432 .map(str::to_string)
2433 .unwrap_or_default();
2434 let keep: &[&str] = match event_type.as_str() {
2435 "subscribed" => &["type", "subscription_id"],
2436 "node.inserted" | "node.updated" => {
2437 &["type", "node_id", "run_id", "metadata", "created_at", "updated_at"]
2438 }
2439 "node.deleted" => &["type", "node_id"],
2440 "memory.added" => &["type", "session_id", "entry"],
2441 _ => return,
2442 };
2443 obj.retain(|k, _| keep.iter().any(|known| *known == k));
2444}
2445
2446fn prune_nulls(value: Value) -> Value {
2447 match value {
2448 Value::Object(map) => Value::Object(
2449 map.into_iter()
2450 .filter_map(|(key, value)| {
2451 let cleaned = prune_nulls(value);
2452 if cleaned.is_null() {
2453 None
2454 } else {
2455 Some((key, cleaned))
2456 }
2457 })
2458 .collect::<Map<String, Value>>(),
2459 ),
2460 Value::Array(items) => Value::Array(items.into_iter().map(prune_nulls).collect()),
2461 other => other,
2462 }
2463}
2464
2465fn resolve_helper_run_id(
2466 explicit: Option<String>,
2467 fallback: Option<String>,
2468 helper_name: &str,
2469) -> Result<String> {
2470 let candidate = explicit.or(fallback).unwrap_or_default();
2471 if candidate.trim().is_empty() {
2472 return Err(SdkError::ValidationError(format!(
2473 "{} requires run_id or a client default run_id",
2474 helper_name
2475 )));
2476 }
2477 Ok(candidate)
2478}
2479
2480fn require_non_empty_string(value: String, field_name: &str) -> Result<String> {
2481 if value.trim().is_empty() {
2482 return Err(SdkError::ValidationError(format!(
2483 "{} is required",
2484 field_name
2485 )));
2486 }
2487 Ok(value)
2488}
2489
2490fn encode_optional_json(value: Option<&Value>) -> Result<String> {
2491 match value {
2492 Some(value) => serde_json::to_string(value).map_err(|err| {
2493 SdkError::ValidationError(format!("failed to serialize helper json field: {}", err))
2494 }),
2495 None => Ok(String::new()),
2496 }
2497}
2498
2499fn encode_string_vec(values: &[String]) -> Result<String> {
2500 if values.is_empty() {
2501 return Ok(String::new());
2502 }
2503 serde_json::to_string(values).map_err(|err| {
2504 SdkError::ValidationError(format!("failed to serialize helper string list: {}", err))
2505 })
2506}
2507
2508fn generate_helper_id(prefix: &str) -> String {
2509 use std::time::{SystemTime, UNIX_EPOCH};
2510
2511 let millis = SystemTime::now()
2512 .duration_since(UNIX_EPOCH)
2513 .unwrap_or_default()
2514 .as_millis();
2515 format!("{}-{}", prefix, millis)
2516}
2517
2518fn ensure_object_payload(payload: Value) -> Result<Value> {
2519 match payload {
2520 Value::Null => Ok(json!({})),
2521 Value::Object(_) => Ok(payload),
2522 _ => Err(SdkError::ValidationError(
2523 "payload must serialize to a JSON object".to_string(),
2524 )),
2525 }
2526}
2527
2528fn decode_grpc_request<T: DeserializeOwned>(op_key: &str, payload: Value) -> Result<T> {
2529 serde_json::from_value(payload).map_err(|e| {
2530 SdkError::ValidationError(format!(
2531 "invalid gRPC request payload for {}: {}",
2532 op_key, e
2533 ))
2534 })
2535}
2536
2537fn encode_grpc_response<T: Serialize>(op_key: &str, response: T) -> Result<Value> {
2538 serde_json::to_value(response).map_err(|e| {
2539 SdkError::ServerError(format!(
2540 "failed to serialize gRPC response for {}: {}",
2541 op_key, e
2542 ))
2543 })
2544}
2545
2546fn decode_batch_insert_payload(
2547 payload: Value,
2548) -> Result<Vec<crate::proto::mubit::v1::InsertRequest>> {
2549 let payload = ensure_object_payload(payload)?;
2550 let mut extracted: Option<&Value> = None;
2551 if let Some(map) = payload.as_object() {
2552 for key in ["items", "requests", "nodes"] {
2553 if let Some(value) = map.get(key) {
2554 extracted = Some(value);
2555 break;
2556 }
2557 }
2558 }
2559
2560 if let Some(Value::Array(items)) = extracted {
2561 if items.is_empty() {
2562 return Err(SdkError::ValidationError(
2563 "batch_insert gRPC payload cannot provide an empty items list".to_string(),
2564 ));
2565 }
2566 return items
2567 .iter()
2568 .cloned()
2569 .map(|item| decode_grpc_request("core.batch_insert", item))
2570 .collect();
2571 }
2572
2573 if extracted.is_some() {
2574 return Err(SdkError::ValidationError(
2575 "batch_insert gRPC payload items/requests/nodes must be an array".to_string(),
2576 ));
2577 }
2578
2579 let single = decode_grpc_request("core.batch_insert", payload)?;
2580 Ok(vec![single])
2581}
2582
2583fn value_to_param(value: &Value) -> Option<String> {
2584 match value {
2585 Value::String(v) => Some(v.clone()),
2586 Value::Number(v) => Some(v.to_string()),
2587 Value::Bool(v) => Some(v.to_string()),
2588 _ => None,
2589 }
2590}
2591
2592fn value_to_query(value: &Value) -> Option<String> {
2593 match value {
2594 Value::String(v) => Some(v.clone()),
2595 Value::Number(v) => Some(v.to_string()),
2596 Value::Bool(v) => Some(v.to_string()),
2597 Value::Array(items) => {
2598 let rendered: Vec<String> = items.iter().filter_map(value_to_query).collect();
2599 if rendered.is_empty() {
2600 None
2601 } else {
2602 Some(rendered.join(","))
2603 }
2604 }
2605 _ => None,
2606 }
2607}
2608
2609fn derive_http_and_grpc(endpoint: &str) -> Result<(String, String, bool)> {
2610 let endpoint = if endpoint.contains("://") {
2611 endpoint.to_string()
2612 } else {
2613 format!("http://{}", endpoint)
2614 };
2615
2616 let parsed = Url::parse(&endpoint).map_err(|e| {
2617 SdkError::ValidationError(format!("invalid endpoint '{}': {}", endpoint, e))
2618 })?;
2619
2620 let host = parsed.host_str().ok_or_else(|| {
2621 SdkError::ValidationError(format!("endpoint '{}' missing host", endpoint))
2622 })?;
2623
2624 let scheme = parsed.scheme();
2625 let port = parsed.port_or_known_default().ok_or_else(|| {
2626 SdkError::ValidationError(format!(
2627 "endpoint '{}' missing known default port",
2628 endpoint
2629 ))
2630 })?;
2631
2632 let default_port = match scheme {
2633 "https" => 443,
2634 _ => 80,
2635 };
2636
2637 let http_endpoint = if port == default_port {
2638 format!("{}://{}", scheme, host)
2639 } else {
2640 format!("{}://{}:{}", scheme, host, port)
2641 };
2642
2643 let grpc_endpoint = format!("{}:{}", host, port);
2644 let grpc_tls = scheme.eq_ignore_ascii_case("https");
2645
2646 Ok((http_endpoint, grpc_endpoint, grpc_tls))
2647}
2648
2649fn normalize_http_endpoint(endpoint: &str) -> Result<String> {
2650 let endpoint = if endpoint.contains("://") {
2651 endpoint.to_string()
2652 } else {
2653 format!("http://{}", endpoint)
2654 };
2655
2656 let parsed = Url::parse(&endpoint).map_err(|e| {
2657 SdkError::ValidationError(format!("invalid http_endpoint '{}': {}", endpoint, e))
2658 })?;
2659
2660 let host = parsed.host_str().ok_or_else(|| {
2661 SdkError::ValidationError(format!("http_endpoint '{}' missing host", endpoint))
2662 })?;
2663
2664 let scheme = parsed.scheme();
2665 let port = parsed.port_or_known_default().ok_or_else(|| {
2666 SdkError::ValidationError(format!(
2667 "http_endpoint '{}' missing known default port",
2668 endpoint
2669 ))
2670 })?;
2671
2672 let default_port = if scheme.eq_ignore_ascii_case("https") {
2673 443
2674 } else {
2675 80
2676 };
2677
2678 let normalized = if port == default_port {
2679 format!("{}://{}", scheme, host)
2680 } else {
2681 format!("{}://{}:{}", scheme, host, port)
2682 };
2683
2684 Ok(normalized)
2685}
2686
2687fn normalize_grpc_endpoint(endpoint: &str) -> Result<(String, bool)> {
2688 if endpoint.contains("://") {
2689 let parsed = Url::parse(endpoint).map_err(|e| {
2690 SdkError::ValidationError(format!("invalid grpc_endpoint '{}': {}", endpoint, e))
2691 })?;
2692 let host = parsed.host_str().ok_or_else(|| {
2693 SdkError::ValidationError(format!("grpc_endpoint '{}' missing host", endpoint))
2694 })?;
2695 let port = parsed.port_or_known_default().ok_or_else(|| {
2696 SdkError::ValidationError(format!(
2697 "grpc_endpoint '{}' missing known default port",
2698 endpoint
2699 ))
2700 })?;
2701 return Ok((
2702 format!("{}:{}", host, port),
2703 parsed.scheme().eq_ignore_ascii_case("https")
2704 || parsed.scheme().eq_ignore_ascii_case("grpcs"),
2705 ));
2706 }
2707
2708 let endpoint = endpoint.trim();
2709 if endpoint.is_empty() {
2710 return Err(SdkError::ValidationError(
2711 "grpc_endpoint cannot be empty".to_string(),
2712 ));
2713 }
2714
2715 if let Some((host, port_text)) = endpoint.rsplit_once(':') {
2716 if !host.trim().is_empty() {
2717 if let Ok(port) = port_text.parse::<u16>() {
2718 return Ok((format!("{}:{}", host, port), port == 443));
2719 }
2720 }
2721 return Ok((endpoint.to_string(), false));
2722 }
2723
2724 Ok((format!("{}:50051", endpoint), false))
2725}
2726
2727fn map_grpc_connect_error(error: tonic::transport::Error, endpoint: &str) -> SdkError {
2728 let lower = error.to_string().to_lowercase();
2729 let kind = if lower.contains("deadline") || lower.contains("timed out") {
2730 TransportFailureKind::DeadlineExceeded
2731 } else if lower.contains("connection reset") {
2732 TransportFailureKind::ConnectionReset
2733 } else if lower.contains("dns")
2734 || lower.contains("refused")
2735 || lower.contains("unavailable")
2736 || lower.contains("not connected")
2737 {
2738 TransportFailureKind::Unavailable
2739 } else {
2740 TransportFailureKind::Io
2741 };
2742
2743 SdkError::TransportError {
2744 kind,
2745 message: format!("failed to connect to gRPC endpoint {}: {}", endpoint, error),
2746 }
2747}
2748
2749fn map_grpc_status(status: Status) -> SdkError {
2750 let message = status.message().to_string();
2751 match status.code() {
2752 Code::Unauthenticated | Code::PermissionDenied => SdkError::AuthError(message),
2753 Code::InvalidArgument
2754 | Code::NotFound
2755 | Code::AlreadyExists
2756 | Code::FailedPrecondition
2757 | Code::OutOfRange => SdkError::ValidationError(message),
2758 Code::Unavailable => SdkError::TransportError {
2759 kind: TransportFailureKind::Unavailable,
2760 message,
2761 },
2762 Code::DeadlineExceeded => SdkError::TransportError {
2763 kind: TransportFailureKind::DeadlineExceeded,
2764 message,
2765 },
2766 Code::Unimplemented => SdkError::TransportError {
2767 kind: TransportFailureKind::Unimplemented,
2768 message,
2769 },
2770 Code::Cancelled => SdkError::TransportError {
2771 kind: TransportFailureKind::Io,
2772 message,
2773 },
2774 Code::Unknown | Code::Internal => {
2775 let lower = message.to_lowercase();
2776 if lower.contains("connection reset") {
2777 SdkError::TransportError {
2778 kind: TransportFailureKind::ConnectionReset,
2779 message,
2780 }
2781 } else if lower.contains("transport")
2782 || lower.contains("broken pipe")
2783 || lower.contains("io error")
2784 {
2785 SdkError::TransportError {
2786 kind: TransportFailureKind::Io,
2787 message,
2788 }
2789 } else {
2790 SdkError::ServerError(message)
2791 }
2792 }
2793 _ => SdkError::ServerError(message),
2794 }
2795}
2796
2797fn map_transport_error(error: reqwest::Error, message: String) -> SdkError {
2798 let lower = error.to_string().to_lowercase();
2799 let kind = if error.is_timeout() {
2800 TransportFailureKind::DeadlineExceeded
2801 } else if error.is_connect() {
2802 TransportFailureKind::Unavailable
2803 } else if lower.contains("connection reset") {
2804 TransportFailureKind::ConnectionReset
2805 } else if error.is_request() || error.is_body() {
2806 TransportFailureKind::Io
2807 } else {
2808 TransportFailureKind::Other
2809 };
2810
2811 SdkError::TransportError {
2812 kind,
2813 message: format!("{}: {}", message, error),
2814 }
2815}
2816
2817fn map_http_error(status: u16, body: String) -> SdkError {
2818 match status {
2819 401 | 403 => SdkError::AuthError(body),
2820 400 | 404 | 409 | 422 => SdkError::ValidationError(body),
2821 501 => SdkError::UnsupportedFeatureError(body),
2822 _ => SdkError::ServerError(body),
2823 }
2824}
2825
2826fn http_method_label(method: HttpMethod) -> &'static str {
2827 match method {
2828 HttpMethod::Get => "GET",
2829 HttpMethod::Post => "POST",
2830 HttpMethod::Delete => "DELETE",
2831 }
2832}