1use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::path::Path;
12use std::sync::Arc;
13
14use crate::{
15 Agent, Config, DiscoveryPage, DiscoveryQuery, Fidelity, HarnessCatalog, Result as CoreResult,
16 Session, SessionDescriptor, SessionLocator,
17};
18
19#[async_trait]
24pub trait SdkPromptSource: Send + Sync {
25 async fn render(&self, args: std::collections::BTreeMap<String, String>) -> CoreResult<String>;
27 fn arg_names(&self) -> &[String];
29}
30
31pub const SDK_SCHEMA_VERSION: &str = "supercode.sdk.v1";
33
34pub fn discover_sessions(query: &DiscoveryQuery) -> CoreResult<Vec<SessionDescriptor>> {
36 Ok(HarnessCatalog::new().discover(query)?)
37}
38
39pub fn discover_session_page(query: &DiscoveryQuery) -> CoreResult<DiscoveryPage> {
41 Ok(HarnessCatalog::new().discover_page(query)?)
42}
43
44pub fn load_session(locator: &SessionLocator) -> CoreResult<Session> {
46 Ok(HarnessCatalog::new().load(locator)?)
47}
48
49pub fn load_session_with_fidelity(
56 locator: &SessionLocator,
57 fidelity: Fidelity,
58) -> CoreResult<Session> {
59 Ok(HarnessCatalog::new().load_with_fidelity(locator, fidelity)?)
60}
61
62pub fn load_session_path(path: &Path, opencode_session: Option<&str>) -> CoreResult<Session> {
65 if opencode_session.is_some() {
66 return Ok(Session::from_opencode_sqlite(path, opencode_session)?);
67 }
68 if let Some(session) = load_native_store_family(path)? {
69 return Ok(session);
70 }
71 Ok(Session::load(path)?)
72}
73
74pub(crate) fn load_native_store_family(path: &Path) -> CoreResult<Option<Session>> {
75 Ok(supercode_interchange::load_native_store_family(path)?)
76}
77
78pub struct SdkAgent(Agent);
86
87impl SdkAgent {
88 pub(crate) fn from_agent(agent: Agent) -> Self {
89 Self(agent)
90 }
91
92 pub(crate) fn inner(&self) -> &Agent {
93 &self.0
94 }
95
96 pub(crate) fn inner_mut(&mut self) -> &mut Agent {
97 &mut self.0
98 }
99
100 pub fn config(&self) -> &Config {
102 self.0.config()
103 }
104
105 pub fn set_recorder(&mut self, writer: crate::sidecar::SidecarWriter) {
107 self.0.set_recorder(writer);
108 }
109
110 pub fn set_reduction_policy(&mut self, policy: crate::reduce::ReductionPolicy) {
112 self.0.set_reduction_policy(policy);
113 }
114
115 pub fn reduction_policy(&self) -> Option<&crate::reduce::ReductionPolicy> {
117 self.0.reduction_policy()
118 }
119
120 pub fn set_reduction_log(&mut self, log: crate::reduce::ReductionLog) {
122 self.0.set_reduction_log(log);
123 }
124
125 pub fn reduction_log(&self) -> &crate::reduce::ReductionLog {
127 self.0.reduction_log()
128 }
129
130 pub fn prepare_cleared_turns_summary(
132 &self,
133 messages: &[crate::ChatMessage],
134 policy: &crate::reduce::ReductionPolicy,
135 prior: &crate::reduce::ReductionLog,
136 ) -> Option<crate::reduce::PreparedClearSummary> {
137 self.0
138 .prepare_cleared_turns_summary(messages, policy, prior)
139 }
140
141 pub fn set_span_summarizer(
143 &mut self,
144 summarizer: impl crate::reduce::summarize::SpanSummarizer + Send + Sync + 'static,
145 ) {
146 self.0.set_span_summarizer(summarizer);
147 }
148
149 pub fn set_session_titler(
151 &mut self,
152 titler: impl crate::session_title::SessionTitler + Send + Sync + 'static,
153 ) {
154 self.0.set_session_titler(titler);
155 }
156
157 pub fn auto_title(&self) -> Option<String> {
159 self.0.auto_title()
160 }
161
162 pub fn set_subagent_store(
164 &mut self,
165 store: std::sync::Arc<crate::SessionStore>,
166 session_name: impl Into<String>,
167 ) {
168 self.0.set_subagent_store(store, session_name);
169 }
170
171 pub fn set_claude_runtime_manifest(
173 &mut self,
174 manifest: crate::claude_runtime_state::ClaudeRuntimeManifest,
175 ) {
176 self.0.set_claude_runtime_manifest(manifest);
177 }
178
179 pub fn claude_runtime_manifest(
181 &self,
182 ) -> Option<&crate::claude_runtime_state::ClaudeRuntimeManifest> {
183 self.0.claude_runtime_manifest()
184 }
185
186 pub fn claude_runtime_manifest_mut(
188 &mut self,
189 ) -> Option<&mut crate::claude_runtime_state::ClaudeRuntimeManifest> {
190 self.0.claude_runtime_manifest_mut()
191 }
192
193 pub fn restore_claude_project_agents(&mut self) -> CoreResult<usize> {
195 self.0.restore_claude_project_agents()
196 }
197
198 pub fn load_session(&mut self, session: Session) {
200 self.0.load_session(session);
201 }
202
203 pub fn load_transcript(&mut self, path: impl AsRef<Path>) -> CoreResult<()> {
205 self.0.load_transcript(path)
206 }
207
208 pub fn save_transcript(&self, path: impl AsRef<Path>) -> CoreResult<()> {
210 self.0.save_transcript(path)
211 }
212
213 pub fn history(&self) -> &[crate::ChatMessage] {
215 self.0.history()
216 }
217
218 pub fn rewind_to(&mut self, checkpoint: usize) {
220 self.0.rewind_to(checkpoint);
221 }
222
223 pub fn append_system_note(&mut self, text: &str) {
225 self.0.append_system_note(text);
226 }
227
228 pub fn register_tool(&mut self, tool: impl crate::Tool + 'static) {
230 self.0.register_tool(tool);
231 }
232
233 pub fn register_mcp_prompt(
235 &mut self,
236 command_name: impl Into<String>,
237 source: impl SdkPromptSource + 'static,
238 ) {
239 self.0.register_mcp_prompt(command_name, source);
240 }
241
242 pub fn tool_schemas(&self) -> Vec<crate::ToolSchema> {
244 self.0.tool_schemas()
245 }
246
247 pub fn set_context_limit(&mut self, limit: u64) {
249 self.0.set_context_limit(limit);
250 }
251
252 pub fn context_limit(&self) -> Option<u64> {
254 self.0.context_limit()
255 }
256
257 pub fn set_model(&mut self, model: impl Into<String>) {
259 self.0.set_model(model);
260 }
261
262 pub fn request_issued(&self) -> bool {
264 self.0.request_issued()
265 }
266
267 pub fn session_name(&self) -> Option<&str> {
269 self.0.session_name()
270 }
271
272 pub fn session_persist(&self) -> bool {
274 self.0.session_persist()
275 }
276
277 pub fn git_metadata(&self) -> Option<&crate::git_metadata::GitMetadataRecord> {
279 self.0.git_metadata()
280 }
281
282 pub fn save_git_metadata(&self, store: &crate::SessionStore, name: &str) -> CoreResult<()> {
284 self.0.save_git_metadata(store, name)
285 }
286
287 pub fn turn_count(&self) -> usize {
289 self.0.turn_count()
290 }
291
292 pub fn total_output_tokens(&self) -> u64 {
294 self.0.total_output_tokens()
295 }
296}
297
298impl From<Agent> for SdkAgent {
299 fn from(agent: Agent) -> Self {
300 Self::from_agent(agent)
301 }
302}
303
304pub fn create_agent(config: Config) -> CoreResult<SdkAgent> {
306 Agent::new(config).map(SdkAgent::from_agent)
307}
308
309pub fn resume_agent(config: Config, session: Session) -> CoreResult<SdkAgent> {
311 Agent::resume(config, session).map(SdkAgent::from_agent)
312}
313
314pub async fn submit_agent(agent: &mut SdkAgent, prompt: &str) -> CoreResult<String> {
316 agent.0.send(prompt).await
317}
318
319pub async fn submit_agent_with_images(
321 agent: &mut SdkAgent,
322 prompt: &str,
323 image_urls: &[String],
324) -> CoreResult<String> {
325 agent.0.send_with_images(prompt, image_urls).await
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
330#[serde(rename_all = "snake_case")]
331pub enum SdkOperation {
332 Discover,
334 Load,
336 Start,
338 Resume,
340 Input,
342 Events,
344 Interrupt,
346 Steer,
348 Respond,
350 Export,
352 Close,
354}
355
356impl SdkOperation {
357 pub const ALL: [Self; 11] = [
359 Self::Discover,
360 Self::Load,
361 Self::Start,
362 Self::Resume,
363 Self::Input,
364 Self::Events,
365 Self::Interrupt,
366 Self::Steer,
367 Self::Respond,
368 Self::Export,
369 Self::Close,
370 ];
371
372 pub const fn method(self) -> Option<&'static str> {
375 match self {
376 Self::Discover => Some("harness.v1.sessions.discover"),
377 Self::Load => Some("harness.v1.sessions.load"),
378 Self::Start => Some("harness.v1.runtimes.start"),
379 Self::Resume => Some("harness.v1.runtimes.resume"),
380 Self::Input => Some("harness.v1.runtimes.send_input"),
381 Self::Events => None,
382 Self::Interrupt => Some("harness.v1.runtimes.interrupt"),
383 Self::Steer => Some("harness.v1.runtimes.steer"),
384 Self::Respond => Some("harness.v1.runtimes.respond"),
385 Self::Export => Some("harness.v1.sessions.export"),
386 Self::Close => Some("harness.v1.runtimes.close"),
387 }
388 }
389
390 pub fn from_method(method: &str) -> Option<Self> {
392 Self::ALL
393 .into_iter()
394 .find(|operation| operation.method() == Some(method))
395 }
396
397 pub const fn action_name(self) -> &'static str {
399 match self {
400 Self::Discover => "discover",
401 Self::Load => "load",
402 Self::Start => "start",
403 Self::Resume => "resume",
404 Self::Input => "input",
405 Self::Events => "events",
406 Self::Interrupt => "interrupt",
407 Self::Steer => "steer",
408 Self::Respond => "respond",
409 Self::Export => "export",
410 Self::Close => "close",
411 }
412 }
413
414 pub fn from_action_name(action: &str) -> Option<Self> {
416 Self::ALL
417 .into_iter()
418 .find(|operation| operation.action_name() == action)
419 }
420}
421
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
424pub struct SdkRequest {
425 pub operation: SdkOperation,
427 #[serde(default)]
429 pub params: Value,
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
434#[serde(rename_all = "snake_case")]
435pub enum SdkErrorCode {
436 Unauthenticated,
438 Unauthorized,
440 ControllerRequired,
442 LeaseExpired,
444 InvalidArgument,
446 NotFound,
448 Busy,
450 UnsupportedAction,
452 Execution,
454 Transport,
456}
457
458#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
460pub enum RuntimeSubmitError {
461 #[error("a turn is already in progress")]
463 Busy,
464 #[error("turn interrupted")]
466 Interrupted,
467 #[error("{0}")]
469 Agent(String),
470}
471
472#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
474pub enum SdkError {
475 #[error("SDK runtime authentication required")]
477 Unauthenticated,
478 #[error("SDK runtime permission `{permission}` is required")]
480 Unauthorized {
481 permission: String,
483 },
484 #[error("controller lease required")]
487 ControllerRequired {
488 holder: Option<String>,
490 expires_at_ms: Option<u64>,
492 },
493 #[error("controller lease expired")]
495 LeaseExpired,
496 #[error("invalid SDK argument for {operation:?}: {message}")]
498 InvalidArgument {
499 operation: SdkOperation,
501 message: String,
503 },
504 #[error("SDK target for {operation:?} was not found: {message}")]
506 NotFound {
507 operation: SdkOperation,
509 message: String,
511 },
512 #[error("SDK action `{0}` is not supported by this runtime")]
514 UnsupportedAction(&'static str),
515 #[error("SDK operation `{0}` is not supported by this runtime")]
517 UnsupportedOperation(String),
518 #[error("SDK event stream lost {0} event(s); reattach for a fresh snapshot")]
520 ReplayGap(u64),
521 #[error("SDK runtime event stream closed")]
523 Closed,
524 #[error("SDK transport failed: {0}")]
526 Transport(String),
527 #[error("SDK request {0} is not pending")]
529 UnknownRequest(u64),
530 #[error("invalid SDK response: {0}")]
532 InvalidResponse(String),
533 #[error(transparent)]
535 Submit(#[from] RuntimeSubmitError),
536 #[error("SDK execution failed for {operation:?}: {message}")]
538 Execution {
539 operation: SdkOperation,
541 message: String,
543 },
544}
545
546impl SdkError {
547 pub fn new(code: SdkErrorCode, operation: SdkOperation, message: impl Into<String>) -> Self {
549 let message = message.into();
550 match code {
551 SdkErrorCode::Unauthenticated => Self::Unauthenticated,
552 SdkErrorCode::Unauthorized => Self::Unauthorized {
553 permission: message,
554 },
555 SdkErrorCode::ControllerRequired => Self::ControllerRequired {
556 holder: None,
557 expires_at_ms: None,
558 },
559 SdkErrorCode::LeaseExpired => Self::LeaseExpired,
560 SdkErrorCode::InvalidArgument => Self::InvalidArgument { operation, message },
561 SdkErrorCode::NotFound => Self::NotFound { operation, message },
562 SdkErrorCode::Busy => Self::Submit(RuntimeSubmitError::Busy),
563 SdkErrorCode::UnsupportedAction => Self::unsupported(operation),
564 SdkErrorCode::Execution => Self::Execution { operation, message },
565 SdkErrorCode::Transport => Self::Transport(message),
566 }
567 }
568
569 pub fn unsupported(operation: SdkOperation) -> Self {
571 Self::UnsupportedAction(operation.action_name())
572 }
573
574 pub fn code(&self) -> SdkErrorCode {
576 match self {
577 Self::Unauthenticated => SdkErrorCode::Unauthenticated,
578 Self::Unauthorized { .. } => SdkErrorCode::Unauthorized,
579 Self::ControllerRequired { .. } => SdkErrorCode::ControllerRequired,
580 Self::LeaseExpired => SdkErrorCode::LeaseExpired,
581 Self::InvalidArgument { .. } | Self::InvalidResponse(_) => {
582 SdkErrorCode::InvalidArgument
583 }
584 Self::NotFound { .. } | Self::UnknownRequest(_) => SdkErrorCode::NotFound,
585 Self::Submit(RuntimeSubmitError::Busy) => SdkErrorCode::Busy,
586 Self::UnsupportedAction(_) | Self::UnsupportedOperation(_) => {
587 SdkErrorCode::UnsupportedAction
588 }
589 Self::Transport(_) | Self::ReplayGap(_) | Self::Closed => SdkErrorCode::Transport,
590 Self::Submit(_) | Self::Execution { .. } => SdkErrorCode::Execution,
591 }
592 }
593
594 pub fn operation(&self) -> Option<SdkOperation> {
596 match self {
597 Self::InvalidArgument { operation, .. }
598 | Self::NotFound { operation, .. }
599 | Self::Execution { operation, .. } => Some(*operation),
600 Self::UnsupportedAction(action) => SdkOperation::from_action_name(action),
601 Self::Unauthenticated
602 | Self::Unauthorized { .. }
603 | Self::ControllerRequired { .. }
604 | Self::LeaseExpired => None,
605 Self::UnknownRequest(_) | Self::InvalidResponse(_) => Some(SdkOperation::Respond),
606 Self::Submit(_) => Some(SdkOperation::Input),
607 Self::UnsupportedOperation(_)
608 | Self::ReplayGap(_)
609 | Self::Closed
610 | Self::Transport(_) => None,
611 }
612 }
613}
614
615#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
617pub struct SdkCapabilities {
618 pub schema_version: String,
620 pub operations: Vec<SdkOperation>,
623 pub error_codes: Vec<SdkErrorCode>,
625 pub opaque_events: bool,
627}
628
629impl Default for SdkCapabilities {
630 fn default() -> Self {
631 Self {
632 schema_version: SDK_SCHEMA_VERSION.into(),
633 operations: SdkOperation::ALL.to_vec(),
634 error_codes: vec![
635 SdkErrorCode::Unauthenticated,
636 SdkErrorCode::Unauthorized,
637 SdkErrorCode::ControllerRequired,
638 SdkErrorCode::LeaseExpired,
639 SdkErrorCode::InvalidArgument,
640 SdkErrorCode::NotFound,
641 SdkErrorCode::Busy,
642 SdkErrorCode::UnsupportedAction,
643 SdkErrorCode::Execution,
644 SdkErrorCode::Transport,
645 ],
646 opaque_events: true,
647 }
648 }
649}
650
651#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
653pub struct SdkEvent {
654 pub sequence: u64,
656 pub kind: String,
658 pub payload: Value,
660}
661
662impl SdkEvent {
663 pub(crate) fn new(sequence: u64, payload: Value) -> Self {
664 let kind = payload
665 .get("type")
666 .or_else(|| payload.get("method"))
667 .and_then(Value::as_str)
668 .unwrap_or("unknown")
669 .to_string();
670 Self {
671 sequence,
672 kind,
673 payload,
674 }
675 }
676}
677
678#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
683pub struct SdkRuntimeEvent {
684 pub session_id: String,
686 pub event: SdkEvent,
688}
689
690#[async_trait]
695pub trait SdkRuntime: Send + Sync {
696 async fn describe(&self) -> Result<crate::frontend::FrontendRuntimeDescriptor, SdkError>;
698 async fn attach(
700 &self,
701 history_limit: usize,
702 ) -> Result<crate::frontend::FrontendAttachment, SdkError>;
703 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), SdkError>;
710 async fn send_input_with_images(
714 self: Arc<Self>,
715 prompt: String,
716 image_urls: Vec<String>,
717 ) -> Result<(), SdkError> {
718 if image_urls.is_empty() {
719 self.send_input(prompt).await
720 } else {
721 Err(SdkError::UnsupportedAction("send_input_attachments"))
722 }
723 }
724 async fn submit(&self, prompt: String) -> Result<String, SdkError>;
726 async fn submit_with_images(
731 &self,
732 prompt: String,
733 image_urls: Vec<String>,
734 ) -> Result<String, SdkError> {
735 if image_urls.is_empty() {
736 self.submit(prompt).await
737 } else {
738 Err(SdkError::UnsupportedAction("submit_attachments"))
739 }
740 }
741 async fn interrupt(&self) -> Result<bool, SdkError>;
743 async fn steer(&self, prompt: String) -> Result<(), SdkError>;
745 async fn respond(&self, response: crate::frontend::FrontendResponse) -> Result<(), SdkError>;
747 async fn invoke(
749 &self,
750 operation: crate::frontend::FrontendOperationInvocation,
751 ) -> Result<crate::frontend::FrontendOperationResult, SdkError> {
752 Err(SdkError::UnsupportedOperation(
753 operation.operation_id().to_string(),
754 ))
755 }
756 async fn lease_snapshot(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
758 Err(SdkError::UnsupportedOperation("runtime.lease".into()))
759 }
760 async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
763 Err(SdkError::UnsupportedOperation(
764 "runtime.take_control".into(),
765 ))
766 }
767 async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
769 Err(SdkError::UnsupportedOperation("runtime.heartbeat".into()))
770 }
771 async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
774 Err(SdkError::UnsupportedOperation("runtime.detach".into()))
775 }
776 async fn close(&self) -> Result<(), SdkError> {
780 Err(SdkError::unsupported(SdkOperation::Close))
781 }
782}
783
784#[async_trait]
786pub trait SdkService: Send {
787 fn capabilities(&self) -> SdkCapabilities {
789 SdkCapabilities::default()
790 }
791
792 async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError>;
795
796 async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError>;
798}