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, DiscoveryQuery, Fidelity, HarnessCatalog, Result as CoreResult, Session,
16 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 HarnessCatalog::new().discover(query)
37}
38
39pub fn load_session(locator: &SessionLocator) -> CoreResult<Session> {
41 HarnessCatalog::new().load(locator)
42}
43
44pub fn load_session_with_fidelity(
51 locator: &SessionLocator,
52 fidelity: Fidelity,
53) -> CoreResult<Session> {
54 HarnessCatalog::new().load_with_fidelity(locator, fidelity)
55}
56
57pub fn load_session_path(path: &Path, opencode_session: Option<&str>) -> CoreResult<Session> {
60 if opencode_session.is_some() {
61 return Session::from_opencode_sqlite(path, opencode_session);
62 }
63 if let Some(session) = load_native_store_family(path)? {
64 return Ok(session);
65 }
66 Session::load(path)
67}
68
69fn load_native_store_family(path: &Path) -> CoreResult<Option<Session>> {
70 let Some(name) = path.file_stem().and_then(|value| value.to_str()) else {
71 return Ok(None);
72 };
73 let Some(parent) = path.parent() else {
74 return Ok(None);
75 };
76 if !parent.join(format!("{name}.meta.json")).is_file() {
77 return Ok(None);
78 }
79 let sidecar = parent.join(format!("{name}.sidecar.jsonl"));
80 let mut session = if sidecar.is_file() {
81 Session::from_sidecar_str(&std::fs::read_to_string(sidecar)?)?
82 } else {
83 let text = std::fs::read_to_string(path)?;
84 let messages = text
85 .lines()
86 .filter(|line| !line.trim().is_empty())
87 .map(serde_json::from_str)
88 .collect::<std::result::Result<Vec<_>, _>>()?;
89 Session::from_native_messages(messages)
90 };
91 session.meta.session_id = Some(name.to_string());
92 Ok(Some(session))
93}
94
95pub struct SdkAgent(Agent);
103
104impl SdkAgent {
105 pub(crate) fn from_agent(agent: Agent) -> Self {
106 Self(agent)
107 }
108
109 pub(crate) fn inner(&self) -> &Agent {
110 &self.0
111 }
112
113 pub(crate) fn inner_mut(&mut self) -> &mut Agent {
114 &mut self.0
115 }
116
117 pub fn config(&self) -> &Config {
119 self.0.config()
120 }
121
122 pub fn set_recorder(&mut self, writer: crate::sidecar::SidecarWriter) {
124 self.0.set_recorder(writer);
125 }
126
127 pub fn set_reduction_policy(&mut self, policy: crate::reduce::ReductionPolicy) {
129 self.0.set_reduction_policy(policy);
130 }
131
132 pub fn reduction_policy(&self) -> Option<&crate::reduce::ReductionPolicy> {
134 self.0.reduction_policy()
135 }
136
137 pub fn set_reduction_log(&mut self, log: crate::reduce::ReductionLog) {
139 self.0.set_reduction_log(log);
140 }
141
142 pub fn reduction_log(&self) -> &crate::reduce::ReductionLog {
144 self.0.reduction_log()
145 }
146
147 pub fn prepare_cleared_turns_summary(
149 &self,
150 messages: &[crate::ChatMessage],
151 policy: &crate::reduce::ReductionPolicy,
152 prior: &crate::reduce::ReductionLog,
153 ) -> Option<crate::reduce::PreparedClearSummary> {
154 self.0
155 .prepare_cleared_turns_summary(messages, policy, prior)
156 }
157
158 pub fn set_span_summarizer(
160 &mut self,
161 summarizer: impl crate::reduce::summarize::SpanSummarizer + Send + Sync + 'static,
162 ) {
163 self.0.set_span_summarizer(summarizer);
164 }
165
166 pub fn set_session_titler(
168 &mut self,
169 titler: impl crate::session_title::SessionTitler + Send + Sync + 'static,
170 ) {
171 self.0.set_session_titler(titler);
172 }
173
174 pub fn auto_title(&self) -> Option<String> {
176 self.0.auto_title()
177 }
178
179 pub fn set_subagent_store(
181 &mut self,
182 store: std::sync::Arc<crate::SessionStore>,
183 session_name: impl Into<String>,
184 ) {
185 self.0.set_subagent_store(store, session_name);
186 }
187
188 pub fn set_claude_runtime_manifest(
190 &mut self,
191 manifest: crate::claude_runtime_state::ClaudeRuntimeManifest,
192 ) {
193 self.0.set_claude_runtime_manifest(manifest);
194 }
195
196 pub fn claude_runtime_manifest(
198 &self,
199 ) -> Option<&crate::claude_runtime_state::ClaudeRuntimeManifest> {
200 self.0.claude_runtime_manifest()
201 }
202
203 pub fn claude_runtime_manifest_mut(
205 &mut self,
206 ) -> Option<&mut crate::claude_runtime_state::ClaudeRuntimeManifest> {
207 self.0.claude_runtime_manifest_mut()
208 }
209
210 pub fn restore_claude_project_agents(&mut self) -> CoreResult<usize> {
212 self.0.restore_claude_project_agents()
213 }
214
215 pub fn load_session(&mut self, session: Session) {
217 self.0.load_session(session);
218 }
219
220 pub fn load_transcript(&mut self, path: impl AsRef<Path>) -> CoreResult<()> {
222 self.0.load_transcript(path)
223 }
224
225 pub fn save_transcript(&self, path: impl AsRef<Path>) -> CoreResult<()> {
227 self.0.save_transcript(path)
228 }
229
230 pub fn history(&self) -> &[crate::ChatMessage] {
232 self.0.history()
233 }
234
235 pub fn rewind_to(&mut self, checkpoint: usize) {
237 self.0.rewind_to(checkpoint);
238 }
239
240 pub fn append_system_note(&mut self, text: &str) {
242 self.0.append_system_note(text);
243 }
244
245 pub fn register_tool(&mut self, tool: impl crate::Tool + 'static) {
247 self.0.register_tool(tool);
248 }
249
250 pub fn register_mcp_prompt(
252 &mut self,
253 command_name: impl Into<String>,
254 source: impl SdkPromptSource + 'static,
255 ) {
256 self.0.register_mcp_prompt(command_name, source);
257 }
258
259 pub fn tool_schemas(&self) -> Vec<crate::ToolSchema> {
261 self.0.tool_schemas()
262 }
263
264 pub fn set_context_limit(&mut self, limit: u64) {
266 self.0.set_context_limit(limit);
267 }
268
269 pub fn context_limit(&self) -> Option<u64> {
271 self.0.context_limit()
272 }
273
274 pub fn set_model(&mut self, model: impl Into<String>) {
276 self.0.set_model(model);
277 }
278
279 pub fn request_issued(&self) -> bool {
281 self.0.request_issued()
282 }
283
284 pub fn session_name(&self) -> Option<&str> {
286 self.0.session_name()
287 }
288
289 pub fn session_persist(&self) -> bool {
291 self.0.session_persist()
292 }
293
294 pub fn git_metadata(&self) -> Option<&crate::git_metadata::GitMetadataRecord> {
296 self.0.git_metadata()
297 }
298
299 pub fn save_git_metadata(&self, store: &crate::SessionStore, name: &str) -> CoreResult<()> {
301 self.0.save_git_metadata(store, name)
302 }
303
304 pub fn turn_count(&self) -> usize {
306 self.0.turn_count()
307 }
308
309 pub fn total_output_tokens(&self) -> u64 {
311 self.0.total_output_tokens()
312 }
313}
314
315impl From<Agent> for SdkAgent {
316 fn from(agent: Agent) -> Self {
317 Self::from_agent(agent)
318 }
319}
320
321pub fn create_agent(config: Config) -> CoreResult<SdkAgent> {
323 Agent::new(config).map(SdkAgent::from_agent)
324}
325
326pub fn resume_agent(config: Config, session: Session) -> CoreResult<SdkAgent> {
328 Agent::resume(config, session).map(SdkAgent::from_agent)
329}
330
331pub async fn submit_agent(agent: &mut SdkAgent, prompt: &str) -> CoreResult<String> {
333 agent.0.send(prompt).await
334}
335
336pub async fn submit_agent_with_images(
338 agent: &mut SdkAgent,
339 prompt: &str,
340 image_urls: &[String],
341) -> CoreResult<String> {
342 agent.0.send_with_images(prompt, image_urls).await
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
347#[serde(rename_all = "snake_case")]
348pub enum SdkOperation {
349 Discover,
351 Load,
353 Start,
355 Resume,
357 Input,
359 Events,
361 Interrupt,
363 Steer,
365 Respond,
367 Export,
369 Close,
371}
372
373impl SdkOperation {
374 pub const ALL: [Self; 11] = [
376 Self::Discover,
377 Self::Load,
378 Self::Start,
379 Self::Resume,
380 Self::Input,
381 Self::Events,
382 Self::Interrupt,
383 Self::Steer,
384 Self::Respond,
385 Self::Export,
386 Self::Close,
387 ];
388
389 pub const fn method(self) -> Option<&'static str> {
392 match self {
393 Self::Discover => Some("harness.v1.sessions.discover"),
394 Self::Load => Some("harness.v1.sessions.load"),
395 Self::Start => Some("harness.v1.runtimes.start"),
396 Self::Resume => Some("harness.v1.runtimes.resume"),
397 Self::Input => Some("harness.v1.runtimes.send_input"),
398 Self::Events => None,
399 Self::Interrupt => Some("harness.v1.runtimes.interrupt"),
400 Self::Steer => Some("harness.v1.runtimes.steer"),
401 Self::Respond => Some("harness.v1.runtimes.respond"),
402 Self::Export => Some("harness.v1.sessions.export"),
403 Self::Close => Some("harness.v1.runtimes.close"),
404 }
405 }
406
407 pub fn from_method(method: &str) -> Option<Self> {
409 Self::ALL
410 .into_iter()
411 .find(|operation| operation.method() == Some(method))
412 }
413
414 pub const fn action_name(self) -> &'static str {
416 match self {
417 Self::Discover => "discover",
418 Self::Load => "load",
419 Self::Start => "start",
420 Self::Resume => "resume",
421 Self::Input => "input",
422 Self::Events => "events",
423 Self::Interrupt => "interrupt",
424 Self::Steer => "steer",
425 Self::Respond => "respond",
426 Self::Export => "export",
427 Self::Close => "close",
428 }
429 }
430
431 pub fn from_action_name(action: &str) -> Option<Self> {
433 Self::ALL
434 .into_iter()
435 .find(|operation| operation.action_name() == action)
436 }
437}
438
439#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
441pub struct SdkRequest {
442 pub operation: SdkOperation,
444 #[serde(default)]
446 pub params: Value,
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
451#[serde(rename_all = "snake_case")]
452pub enum SdkErrorCode {
453 Unauthenticated,
455 Unauthorized,
457 ControllerRequired,
459 LeaseExpired,
461 InvalidArgument,
463 NotFound,
465 Busy,
467 UnsupportedAction,
469 Execution,
471 Transport,
473}
474
475#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
477pub enum RuntimeSubmitError {
478 #[error("a turn is already in progress")]
480 Busy,
481 #[error("turn interrupted")]
483 Interrupted,
484 #[error("{0}")]
486 Agent(String),
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
491pub enum SdkError {
492 #[error("SDK runtime authentication required")]
494 Unauthenticated,
495 #[error("SDK runtime permission `{permission}` is required")]
497 Unauthorized {
498 permission: String,
500 },
501 #[error("controller lease required")]
504 ControllerRequired {
505 holder: Option<String>,
507 expires_at_ms: Option<u64>,
509 },
510 #[error("controller lease expired")]
512 LeaseExpired,
513 #[error("invalid SDK argument for {operation:?}: {message}")]
515 InvalidArgument {
516 operation: SdkOperation,
518 message: String,
520 },
521 #[error("SDK target for {operation:?} was not found: {message}")]
523 NotFound {
524 operation: SdkOperation,
526 message: String,
528 },
529 #[error("SDK action `{0}` is not supported by this runtime")]
531 UnsupportedAction(&'static str),
532 #[error("SDK operation `{0}` is not supported by this runtime")]
534 UnsupportedOperation(String),
535 #[error("SDK event stream lost {0} event(s); reattach for a fresh snapshot")]
537 ReplayGap(u64),
538 #[error("SDK runtime event stream closed")]
540 Closed,
541 #[error("SDK transport failed: {0}")]
543 Transport(String),
544 #[error("SDK request {0} is not pending")]
546 UnknownRequest(u64),
547 #[error("invalid SDK response: {0}")]
549 InvalidResponse(String),
550 #[error(transparent)]
552 Submit(#[from] RuntimeSubmitError),
553 #[error("SDK execution failed for {operation:?}: {message}")]
555 Execution {
556 operation: SdkOperation,
558 message: String,
560 },
561}
562
563impl SdkError {
564 pub fn new(code: SdkErrorCode, operation: SdkOperation, message: impl Into<String>) -> Self {
566 let message = message.into();
567 match code {
568 SdkErrorCode::Unauthenticated => Self::Unauthenticated,
569 SdkErrorCode::Unauthorized => Self::Unauthorized {
570 permission: message,
571 },
572 SdkErrorCode::ControllerRequired => Self::ControllerRequired {
573 holder: None,
574 expires_at_ms: None,
575 },
576 SdkErrorCode::LeaseExpired => Self::LeaseExpired,
577 SdkErrorCode::InvalidArgument => Self::InvalidArgument { operation, message },
578 SdkErrorCode::NotFound => Self::NotFound { operation, message },
579 SdkErrorCode::Busy => Self::Submit(RuntimeSubmitError::Busy),
580 SdkErrorCode::UnsupportedAction => Self::unsupported(operation),
581 SdkErrorCode::Execution => Self::Execution { operation, message },
582 SdkErrorCode::Transport => Self::Transport(message),
583 }
584 }
585
586 pub fn unsupported(operation: SdkOperation) -> Self {
588 Self::UnsupportedAction(operation.action_name())
589 }
590
591 pub fn code(&self) -> SdkErrorCode {
593 match self {
594 Self::Unauthenticated => SdkErrorCode::Unauthenticated,
595 Self::Unauthorized { .. } => SdkErrorCode::Unauthorized,
596 Self::ControllerRequired { .. } => SdkErrorCode::ControllerRequired,
597 Self::LeaseExpired => SdkErrorCode::LeaseExpired,
598 Self::InvalidArgument { .. } | Self::InvalidResponse(_) => {
599 SdkErrorCode::InvalidArgument
600 }
601 Self::NotFound { .. } | Self::UnknownRequest(_) => SdkErrorCode::NotFound,
602 Self::Submit(RuntimeSubmitError::Busy) => SdkErrorCode::Busy,
603 Self::UnsupportedAction(_) | Self::UnsupportedOperation(_) => {
604 SdkErrorCode::UnsupportedAction
605 }
606 Self::Transport(_) | Self::ReplayGap(_) | Self::Closed => SdkErrorCode::Transport,
607 Self::Submit(_) | Self::Execution { .. } => SdkErrorCode::Execution,
608 }
609 }
610
611 pub fn operation(&self) -> Option<SdkOperation> {
613 match self {
614 Self::InvalidArgument { operation, .. }
615 | Self::NotFound { operation, .. }
616 | Self::Execution { operation, .. } => Some(*operation),
617 Self::UnsupportedAction(action) => SdkOperation::from_action_name(action),
618 Self::Unauthenticated
619 | Self::Unauthorized { .. }
620 | Self::ControllerRequired { .. }
621 | Self::LeaseExpired => None,
622 Self::UnknownRequest(_) | Self::InvalidResponse(_) => Some(SdkOperation::Respond),
623 Self::Submit(_) => Some(SdkOperation::Input),
624 Self::UnsupportedOperation(_)
625 | Self::ReplayGap(_)
626 | Self::Closed
627 | Self::Transport(_) => None,
628 }
629 }
630}
631
632#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
634pub struct SdkCapabilities {
635 pub schema_version: String,
637 pub operations: Vec<SdkOperation>,
640 pub error_codes: Vec<SdkErrorCode>,
642 pub opaque_events: bool,
644}
645
646impl Default for SdkCapabilities {
647 fn default() -> Self {
648 Self {
649 schema_version: SDK_SCHEMA_VERSION.into(),
650 operations: SdkOperation::ALL.to_vec(),
651 error_codes: vec![
652 SdkErrorCode::Unauthenticated,
653 SdkErrorCode::Unauthorized,
654 SdkErrorCode::ControllerRequired,
655 SdkErrorCode::LeaseExpired,
656 SdkErrorCode::InvalidArgument,
657 SdkErrorCode::NotFound,
658 SdkErrorCode::Busy,
659 SdkErrorCode::UnsupportedAction,
660 SdkErrorCode::Execution,
661 SdkErrorCode::Transport,
662 ],
663 opaque_events: true,
664 }
665 }
666}
667
668#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
670pub struct SdkEvent {
671 pub sequence: u64,
673 pub kind: String,
675 pub payload: Value,
677}
678
679impl SdkEvent {
680 pub(crate) fn new(sequence: u64, payload: Value) -> Self {
681 let kind = payload
682 .get("type")
683 .or_else(|| payload.get("method"))
684 .and_then(Value::as_str)
685 .unwrap_or("unknown")
686 .to_string();
687 Self {
688 sequence,
689 kind,
690 payload,
691 }
692 }
693}
694
695#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
700pub struct SdkRuntimeEvent {
701 pub session_id: String,
703 pub event: SdkEvent,
705}
706
707#[async_trait]
712pub trait SdkRuntime: Send + Sync {
713 async fn describe(&self) -> Result<crate::frontend::FrontendRuntimeDescriptor, SdkError>;
715 async fn attach(
717 &self,
718 history_limit: usize,
719 ) -> Result<crate::frontend::FrontendAttachment, SdkError>;
720 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), SdkError>;
727 async fn submit(&self, prompt: String) -> Result<String, SdkError>;
729 async fn submit_with_images(
734 &self,
735 prompt: String,
736 image_urls: Vec<String>,
737 ) -> Result<String, SdkError> {
738 if image_urls.is_empty() {
739 self.submit(prompt).await
740 } else {
741 Err(SdkError::UnsupportedAction("submit_attachments"))
742 }
743 }
744 async fn interrupt(&self) -> Result<bool, SdkError>;
746 async fn steer(&self, prompt: String) -> Result<(), SdkError>;
748 async fn respond(&self, response: crate::frontend::FrontendResponse) -> Result<(), SdkError>;
750 async fn invoke(
752 &self,
753 operation: crate::frontend::FrontendOperationInvocation,
754 ) -> Result<crate::frontend::FrontendOperationResult, SdkError> {
755 Err(SdkError::UnsupportedOperation(
756 operation.operation_id().to_string(),
757 ))
758 }
759 async fn lease_snapshot(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
761 Err(SdkError::UnsupportedOperation("runtime.lease".into()))
762 }
763 async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
766 Err(SdkError::UnsupportedOperation(
767 "runtime.take_control".into(),
768 ))
769 }
770 async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
772 Err(SdkError::UnsupportedOperation("runtime.heartbeat".into()))
773 }
774 async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
777 Err(SdkError::UnsupportedOperation("runtime.detach".into()))
778 }
779 async fn close(&self) -> Result<(), SdkError> {
783 Err(SdkError::unsupported(SdkOperation::Close))
784 }
785}
786
787#[async_trait]
789pub trait SdkService: Send {
790 fn capabilities(&self) -> SdkCapabilities {
792 SdkCapabilities::default()
793 }
794
795 async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError>;
798
799 async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError>;
801}