use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::Path;
use std::sync::Arc;
use crate::{
Agent, Config, DiscoveryPage, DiscoveryQuery, Fidelity, HarnessCatalog, Result as CoreResult,
Session, SessionDescriptor, SessionLocator,
};
#[async_trait]
pub trait SdkPromptSource: Send + Sync {
async fn render(&self, args: std::collections::BTreeMap<String, String>) -> CoreResult<String>;
fn arg_names(&self) -> &[String];
}
pub const SDK_SCHEMA_VERSION: &str = "supercode.sdk.v1";
pub fn discover_sessions(query: &DiscoveryQuery) -> CoreResult<Vec<SessionDescriptor>> {
Ok(HarnessCatalog::new().discover(query)?)
}
pub fn discover_session_page(query: &DiscoveryQuery) -> CoreResult<DiscoveryPage> {
Ok(HarnessCatalog::new().discover_page(query)?)
}
pub fn load_session(locator: &SessionLocator) -> CoreResult<Session> {
Ok(HarnessCatalog::new().load(locator)?)
}
pub fn load_session_with_fidelity(
locator: &SessionLocator,
fidelity: Fidelity,
) -> CoreResult<Session> {
Ok(HarnessCatalog::new().load_with_fidelity(locator, fidelity)?)
}
pub fn load_session_path(path: &Path, opencode_session: Option<&str>) -> CoreResult<Session> {
if opencode_session.is_some() {
return Ok(Session::from_opencode_sqlite(path, opencode_session)?);
}
if let Some(session) = load_native_store_family(path)? {
return Ok(session);
}
Ok(Session::load(path)?)
}
pub(crate) fn load_native_store_family(path: &Path) -> CoreResult<Option<Session>> {
Ok(supercode_interchange::load_native_store_family(path)?)
}
pub struct SdkAgent(Agent);
impl SdkAgent {
pub(crate) fn from_agent(agent: Agent) -> Self {
Self(agent)
}
pub(crate) fn inner(&self) -> &Agent {
&self.0
}
pub(crate) fn inner_mut(&mut self) -> &mut Agent {
&mut self.0
}
pub fn config(&self) -> &Config {
self.0.config()
}
pub fn set_recorder(&mut self, writer: crate::sidecar::SidecarWriter) {
self.0.set_recorder(writer);
}
pub fn set_reduction_policy(&mut self, policy: crate::reduce::ReductionPolicy) {
self.0.set_reduction_policy(policy);
}
pub fn reduction_policy(&self) -> Option<&crate::reduce::ReductionPolicy> {
self.0.reduction_policy()
}
pub fn set_reduction_log(&mut self, log: crate::reduce::ReductionLog) {
self.0.set_reduction_log(log);
}
pub fn reduction_log(&self) -> &crate::reduce::ReductionLog {
self.0.reduction_log()
}
pub fn prepare_cleared_turns_summary(
&self,
messages: &[crate::ChatMessage],
policy: &crate::reduce::ReductionPolicy,
prior: &crate::reduce::ReductionLog,
) -> Option<crate::reduce::PreparedClearSummary> {
self.0
.prepare_cleared_turns_summary(messages, policy, prior)
}
pub fn set_span_summarizer(
&mut self,
summarizer: impl crate::reduce::summarize::SpanSummarizer + Send + Sync + 'static,
) {
self.0.set_span_summarizer(summarizer);
}
pub fn set_session_titler(
&mut self,
titler: impl crate::session_title::SessionTitler + Send + Sync + 'static,
) {
self.0.set_session_titler(titler);
}
pub fn auto_title(&self) -> Option<String> {
self.0.auto_title()
}
pub fn set_subagent_store(
&mut self,
store: std::sync::Arc<crate::SessionStore>,
session_name: impl Into<String>,
) {
self.0.set_subagent_store(store, session_name);
}
pub fn set_claude_runtime_manifest(
&mut self,
manifest: crate::claude_runtime_state::ClaudeRuntimeManifest,
) {
self.0.set_claude_runtime_manifest(manifest);
}
pub fn claude_runtime_manifest(
&self,
) -> Option<&crate::claude_runtime_state::ClaudeRuntimeManifest> {
self.0.claude_runtime_manifest()
}
pub fn claude_runtime_manifest_mut(
&mut self,
) -> Option<&mut crate::claude_runtime_state::ClaudeRuntimeManifest> {
self.0.claude_runtime_manifest_mut()
}
pub fn restore_claude_project_agents(&mut self) -> CoreResult<usize> {
self.0.restore_claude_project_agents()
}
pub fn load_session(&mut self, session: Session) {
self.0.load_session(session);
}
pub fn load_transcript(&mut self, path: impl AsRef<Path>) -> CoreResult<()> {
self.0.load_transcript(path)
}
pub fn save_transcript(&self, path: impl AsRef<Path>) -> CoreResult<()> {
self.0.save_transcript(path)
}
pub fn history(&self) -> &[crate::ChatMessage] {
self.0.history()
}
pub fn rewind_to(&mut self, checkpoint: usize) {
self.0.rewind_to(checkpoint);
}
pub fn append_system_note(&mut self, text: &str) {
self.0.append_system_note(text);
}
pub fn register_tool(&mut self, tool: impl crate::Tool + 'static) {
self.0.register_tool(tool);
}
pub fn register_mcp_prompt(
&mut self,
command_name: impl Into<String>,
source: impl SdkPromptSource + 'static,
) {
self.0.register_mcp_prompt(command_name, source);
}
pub fn tool_schemas(&self) -> Vec<crate::ToolSchema> {
self.0.tool_schemas()
}
pub fn set_context_limit(&mut self, limit: u64) {
self.0.set_context_limit(limit);
}
pub fn context_limit(&self) -> Option<u64> {
self.0.context_limit()
}
pub fn set_model(&mut self, model: impl Into<String>) {
self.0.set_model(model);
}
pub fn request_issued(&self) -> bool {
self.0.request_issued()
}
pub fn session_name(&self) -> Option<&str> {
self.0.session_name()
}
pub fn session_persist(&self) -> bool {
self.0.session_persist()
}
pub fn git_metadata(&self) -> Option<&crate::git_metadata::GitMetadataRecord> {
self.0.git_metadata()
}
pub fn save_git_metadata(&self, store: &crate::SessionStore, name: &str) -> CoreResult<()> {
self.0.save_git_metadata(store, name)
}
pub fn turn_count(&self) -> usize {
self.0.turn_count()
}
pub fn total_output_tokens(&self) -> u64 {
self.0.total_output_tokens()
}
}
impl From<Agent> for SdkAgent {
fn from(agent: Agent) -> Self {
Self::from_agent(agent)
}
}
pub fn create_agent(config: Config) -> CoreResult<SdkAgent> {
Agent::new(config).map(SdkAgent::from_agent)
}
pub fn resume_agent(config: Config, session: Session) -> CoreResult<SdkAgent> {
Agent::resume(config, session).map(SdkAgent::from_agent)
}
pub async fn submit_agent(agent: &mut SdkAgent, prompt: &str) -> CoreResult<String> {
agent.0.send(prompt).await
}
pub async fn submit_agent_with_images(
agent: &mut SdkAgent,
prompt: &str,
image_urls: &[String],
) -> CoreResult<String> {
agent.0.send_with_images(prompt, image_urls).await
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SdkOperation {
Discover,
Load,
Start,
Resume,
Input,
Events,
Interrupt,
Steer,
Respond,
Export,
Close,
}
impl SdkOperation {
pub const ALL: [Self; 11] = [
Self::Discover,
Self::Load,
Self::Start,
Self::Resume,
Self::Input,
Self::Events,
Self::Interrupt,
Self::Steer,
Self::Respond,
Self::Export,
Self::Close,
];
pub const fn method(self) -> Option<&'static str> {
match self {
Self::Discover => Some("harness.v1.sessions.discover"),
Self::Load => Some("harness.v1.sessions.load"),
Self::Start => Some("harness.v1.runtimes.start"),
Self::Resume => Some("harness.v1.runtimes.resume"),
Self::Input => Some("harness.v1.runtimes.send_input"),
Self::Events => None,
Self::Interrupt => Some("harness.v1.runtimes.interrupt"),
Self::Steer => Some("harness.v1.runtimes.steer"),
Self::Respond => Some("harness.v1.runtimes.respond"),
Self::Export => Some("harness.v1.sessions.export"),
Self::Close => Some("harness.v1.runtimes.close"),
}
}
pub fn from_method(method: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|operation| operation.method() == Some(method))
}
pub const fn action_name(self) -> &'static str {
match self {
Self::Discover => "discover",
Self::Load => "load",
Self::Start => "start",
Self::Resume => "resume",
Self::Input => "input",
Self::Events => "events",
Self::Interrupt => "interrupt",
Self::Steer => "steer",
Self::Respond => "respond",
Self::Export => "export",
Self::Close => "close",
}
}
pub fn from_action_name(action: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|operation| operation.action_name() == action)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SdkRequest {
pub operation: SdkOperation,
#[serde(default)]
pub params: Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SdkErrorCode {
Unauthenticated,
Unauthorized,
ControllerRequired,
LeaseExpired,
InvalidArgument,
NotFound,
Busy,
UnsupportedAction,
Execution,
Transport,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RuntimeSubmitError {
#[error("a turn is already in progress")]
Busy,
#[error("turn interrupted")]
Interrupted,
#[error("{0}")]
Agent(String),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SdkError {
#[error("SDK runtime authentication required")]
Unauthenticated,
#[error("SDK runtime permission `{permission}` is required")]
Unauthorized {
permission: String,
},
#[error("controller lease required")]
ControllerRequired {
holder: Option<String>,
expires_at_ms: Option<u64>,
},
#[error("controller lease expired")]
LeaseExpired,
#[error("invalid SDK argument for {operation:?}: {message}")]
InvalidArgument {
operation: SdkOperation,
message: String,
},
#[error("SDK target for {operation:?} was not found: {message}")]
NotFound {
operation: SdkOperation,
message: String,
},
#[error("SDK action `{0}` is not supported by this runtime")]
UnsupportedAction(&'static str),
#[error("SDK operation `{0}` is not supported by this runtime")]
UnsupportedOperation(String),
#[error("SDK event stream lost {0} event(s); reattach for a fresh snapshot")]
ReplayGap(u64),
#[error("SDK runtime event stream closed")]
Closed,
#[error("SDK transport failed: {0}")]
Transport(String),
#[error("SDK request {0} is not pending")]
UnknownRequest(u64),
#[error("invalid SDK response: {0}")]
InvalidResponse(String),
#[error(transparent)]
Submit(#[from] RuntimeSubmitError),
#[error("SDK execution failed for {operation:?}: {message}")]
Execution {
operation: SdkOperation,
message: String,
},
}
impl SdkError {
pub fn new(code: SdkErrorCode, operation: SdkOperation, message: impl Into<String>) -> Self {
let message = message.into();
match code {
SdkErrorCode::Unauthenticated => Self::Unauthenticated,
SdkErrorCode::Unauthorized => Self::Unauthorized {
permission: message,
},
SdkErrorCode::ControllerRequired => Self::ControllerRequired {
holder: None,
expires_at_ms: None,
},
SdkErrorCode::LeaseExpired => Self::LeaseExpired,
SdkErrorCode::InvalidArgument => Self::InvalidArgument { operation, message },
SdkErrorCode::NotFound => Self::NotFound { operation, message },
SdkErrorCode::Busy => Self::Submit(RuntimeSubmitError::Busy),
SdkErrorCode::UnsupportedAction => Self::unsupported(operation),
SdkErrorCode::Execution => Self::Execution { operation, message },
SdkErrorCode::Transport => Self::Transport(message),
}
}
pub fn unsupported(operation: SdkOperation) -> Self {
Self::UnsupportedAction(operation.action_name())
}
pub fn code(&self) -> SdkErrorCode {
match self {
Self::Unauthenticated => SdkErrorCode::Unauthenticated,
Self::Unauthorized { .. } => SdkErrorCode::Unauthorized,
Self::ControllerRequired { .. } => SdkErrorCode::ControllerRequired,
Self::LeaseExpired => SdkErrorCode::LeaseExpired,
Self::InvalidArgument { .. } | Self::InvalidResponse(_) => {
SdkErrorCode::InvalidArgument
}
Self::NotFound { .. } | Self::UnknownRequest(_) => SdkErrorCode::NotFound,
Self::Submit(RuntimeSubmitError::Busy) => SdkErrorCode::Busy,
Self::UnsupportedAction(_) | Self::UnsupportedOperation(_) => {
SdkErrorCode::UnsupportedAction
}
Self::Transport(_) | Self::ReplayGap(_) | Self::Closed => SdkErrorCode::Transport,
Self::Submit(_) | Self::Execution { .. } => SdkErrorCode::Execution,
}
}
pub fn operation(&self) -> Option<SdkOperation> {
match self {
Self::InvalidArgument { operation, .. }
| Self::NotFound { operation, .. }
| Self::Execution { operation, .. } => Some(*operation),
Self::UnsupportedAction(action) => SdkOperation::from_action_name(action),
Self::Unauthenticated
| Self::Unauthorized { .. }
| Self::ControllerRequired { .. }
| Self::LeaseExpired => None,
Self::UnknownRequest(_) | Self::InvalidResponse(_) => Some(SdkOperation::Respond),
Self::Submit(_) => Some(SdkOperation::Input),
Self::UnsupportedOperation(_)
| Self::ReplayGap(_)
| Self::Closed
| Self::Transport(_) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SdkCapabilities {
pub schema_version: String,
pub operations: Vec<SdkOperation>,
pub error_codes: Vec<SdkErrorCode>,
pub opaque_events: bool,
}
impl Default for SdkCapabilities {
fn default() -> Self {
Self {
schema_version: SDK_SCHEMA_VERSION.into(),
operations: SdkOperation::ALL.to_vec(),
error_codes: vec![
SdkErrorCode::Unauthenticated,
SdkErrorCode::Unauthorized,
SdkErrorCode::ControllerRequired,
SdkErrorCode::LeaseExpired,
SdkErrorCode::InvalidArgument,
SdkErrorCode::NotFound,
SdkErrorCode::Busy,
SdkErrorCode::UnsupportedAction,
SdkErrorCode::Execution,
SdkErrorCode::Transport,
],
opaque_events: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SdkEvent {
pub sequence: u64,
pub kind: String,
pub payload: Value,
}
impl SdkEvent {
pub(crate) fn new(sequence: u64, payload: Value) -> Self {
let kind = payload
.get("type")
.or_else(|| payload.get("method"))
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string();
Self {
sequence,
kind,
payload,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SdkRuntimeEvent {
pub session_id: String,
pub event: SdkEvent,
}
#[async_trait]
pub trait SdkRuntime: Send + Sync {
async fn describe(&self) -> Result<crate::frontend::FrontendRuntimeDescriptor, SdkError>;
async fn attach(
&self,
history_limit: usize,
) -> Result<crate::frontend::FrontendAttachment, SdkError>;
async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), SdkError>;
async fn submit(&self, prompt: String) -> Result<String, SdkError>;
async fn submit_with_images(
&self,
prompt: String,
image_urls: Vec<String>,
) -> Result<String, SdkError> {
if image_urls.is_empty() {
self.submit(prompt).await
} else {
Err(SdkError::UnsupportedAction("submit_attachments"))
}
}
async fn interrupt(&self) -> Result<bool, SdkError>;
async fn steer(&self, prompt: String) -> Result<(), SdkError>;
async fn respond(&self, response: crate::frontend::FrontendResponse) -> Result<(), SdkError>;
async fn invoke(
&self,
operation: crate::frontend::FrontendOperationInvocation,
) -> Result<crate::frontend::FrontendOperationResult, SdkError> {
Err(SdkError::UnsupportedOperation(
operation.operation_id().to_string(),
))
}
async fn lease_snapshot(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
Err(SdkError::UnsupportedOperation("runtime.lease".into()))
}
async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
Err(SdkError::UnsupportedOperation(
"runtime.take_control".into(),
))
}
async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
Err(SdkError::UnsupportedOperation("runtime.heartbeat".into()))
}
async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, SdkError> {
Err(SdkError::UnsupportedOperation("runtime.detach".into()))
}
async fn close(&self) -> Result<(), SdkError> {
Err(SdkError::unsupported(SdkOperation::Close))
}
}
#[async_trait]
pub trait SdkService: Send {
fn capabilities(&self) -> SdkCapabilities {
SdkCapabilities::default()
}
async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError>;
async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError>;
}