use super::{SpacesdClient, run};
use crate::{CuaError, Result};
use cua_agents as ca;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
impl From<ca::Error> for CuaError {
fn from(e: ca::Error) -> Self {
match e {
ca::Error::Invalid(m) => CuaError::InvalidArgument(m),
ca::Error::NotFound(m) => CuaError::NotFound(m),
ca::Error::Unsupported(m) => CuaError::Unsupported(m),
ca::Error::Timeout(m) => CuaError::Timeout(m),
ca::Error::Client(c) => c.into(),
other => CuaError::Env(other.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, uniffi::Record)]
pub struct AgentRunMcpServer {
pub name: String,
#[uniffi(default = None)]
pub url: Option<String>,
#[uniffi(default)]
pub headers: HashMap<String, String>,
#[uniffi(default = None)]
pub command: Option<String>,
#[uniffi(default = [])]
pub args: Vec<String>,
#[uniffi(default)]
pub env: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct AgentFile {
pub name: String,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, uniffi::Record)]
pub struct AgentRunOptions {
#[uniffi(default = None)]
pub cwd: Option<String>,
#[uniffi(default = None)]
pub repo: Option<String>,
#[uniffi(default = None)]
pub branch: Option<String>,
#[uniffi(default)]
pub env: HashMap<String, String>,
#[uniffi(default = [])]
pub env_from_host: Vec<String>,
#[uniffi(default = None)]
pub model: Option<String>,
#[uniffi(default = None)]
pub base_url: Option<String>,
#[uniffi(default = None)]
pub wire: Option<String>,
#[uniffi(default = [])]
pub mcp_servers: Vec<AgentRunMcpServer>,
#[uniffi(default = None)]
pub sandbox_mcp: Option<bool>,
#[uniffi(default = None)]
pub skills: Option<bool>,
#[uniffi(default = None)]
pub install: Option<bool>,
#[uniffi(default = [])]
pub files: Vec<AgentFile>,
#[uniffi(default = false)]
pub exit_when_idle: bool,
#[uniffi(default = None)]
pub label: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct AgentEvent {
pub seq: u64,
pub ts_ms: u64,
pub turn: u32,
pub kind: String,
pub text: Option<String>,
pub tool_id: Option<String>,
pub tool_title: Option<String>,
pub tool_kind: Option<String>,
pub tool_status: Option<String>,
pub stop_reason: Option<String>,
pub category: String,
pub summary: Option<String>,
pub line: Option<String>,
pub json: String,
}
impl From<&ca::AgentEvent> for AgentEvent {
fn from(e: &ca::AgentEvent) -> Self {
AgentEvent {
seq: e.seq,
ts_ms: e.ts_ms,
turn: e.turn,
kind: e.kind.into(),
text: e.text.clone(),
tool_id: e.tool_id.clone(),
tool_title: e.tool_title.clone(),
tool_kind: e.tool_kind.clone(),
tool_status: e.tool_status.clone(),
stop_reason: e.stop_reason.clone(),
category: e.category.into(),
summary: e.summary.clone(),
line: e.render(),
json: e.raw.to_string(),
}
}
}
#[uniffi::export]
pub fn agent_event_category(kind: String) -> String {
ca::events::category(&kind).into()
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct AgentTranscriptItem {
pub kind: String,
pub turn: u32,
pub text: String,
pub steps: Vec<String>,
}
impl From<&ca::TranscriptItem> for AgentTranscriptItem {
fn from(i: &ca::TranscriptItem) -> Self {
AgentTranscriptItem {
kind: i.kind.into(),
turn: i.turn,
text: i.text.clone(),
steps: i.steps.clone(),
}
}
}
#[derive(uniffi::Object)]
pub struct AgentTranscript {
inner: std::sync::Mutex<(ca::Transcript, u64)>,
}
#[uniffi::export]
impl AgentTranscript {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(AgentTranscript {
inner: Default::default(),
})
}
pub fn absorb(&self, events: Vec<AgentEvent>) {
let mut g = self.inner.lock().unwrap_or_else(|p| p.into_inner());
for e in events {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&e.json)
&& let Some(parsed) = ca::AgentEvent::from_value(&v)
{
g.0.absorb(&parsed);
}
}
}
pub fn absorb_json(&self, json: String) -> Result<Option<u64>> {
let v: serde_json::Value = serde_json::from_str(&json)?;
let (events, cursor) = match &v {
serde_json::Value::Array(a) => (a.as_slice(), None),
serde_json::Value::Object(o) => (
o.get("events")
.and_then(|e| e.as_array())
.map(Vec::as_slice)
.unwrap_or_default(),
o.get("cursor").and_then(|c| c.as_u64()),
),
_ => {
return Err(CuaError::InvalidArgument(
"absorb_json takes an agent_events result or an array of events".into(),
));
}
};
let mut g = self.inner.lock().unwrap_or_else(|p| p.into_inner());
for e in events.iter().filter_map(ca::AgentEvent::from_value) {
g.0.absorb(&e);
}
if let Some(c) = cursor {
g.1 = g.1.max(c);
}
Ok(cursor)
}
pub fn note(&self, turn: u32, text: String) {
let mut g = self.inner.lock().unwrap_or_else(|p| p.into_inner());
g.0.note(turn, &text);
}
pub fn items(&self) -> Vec<AgentTranscriptItem> {
let g = self.inner.lock().unwrap_or_else(|p| p.into_inner());
g.0.items().iter().map(Into::into).collect()
}
pub fn preview(&self) -> Option<String> {
let g = self.inner.lock().unwrap_or_else(|p| p.into_inner());
g.0.preview()
}
pub fn revision(&self) -> u64 {
let g = self.inner.lock().unwrap_or_else(|p| p.into_inner());
g.0.revision()
}
pub fn cursor(&self) -> u64 {
let g = self.inner.lock().unwrap_or_else(|p| p.into_inner());
g.1
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct AgentEventPage {
pub events: Vec<AgentEvent>,
pub cursor: u64,
pub caught_up: bool,
}
#[derive(Debug, Clone, PartialEq, uniffi::Record)]
pub struct AgentRunInfo {
pub run_id: String,
pub harness: Option<String>,
pub status: String,
pub phase: String,
pub reason: String,
pub turn: u32,
pub alive: Option<bool>,
pub accepts_message: bool,
pub prompt: Option<String>,
pub label: Option<String>,
pub created_at: Option<f64>,
pub json: String,
}
impl From<ca::RunInfo> for AgentRunInfo {
fn from(r: ca::RunInfo) -> Self {
AgentRunInfo {
json: serde_json::to_string(&r).unwrap_or_default(),
run_id: r.run_id,
harness: r.harness,
status: r.status.as_str().into(),
phase: r.phase,
reason: r.reason,
turn: r.turn,
alive: r.alive,
accepts_message: r.accepts_message,
prompt: r.meta.as_ref().map(|m| m.prompt.clone()),
label: r.meta.as_ref().and_then(|m| m.label.clone()),
created_at: r.meta.as_ref().map(|m| m.created_at),
}
}
}
#[derive(Debug, Clone, PartialEq, uniffi::Record)]
pub struct AgentRunResult {
pub run_id: String,
pub status: String,
pub turn: u32,
pub text: String,
pub stop_reason: Option<String>,
pub usage_json: Option<String>,
pub error: Option<String>,
pub tool_calls: u32,
}
impl From<ca::RunResult> for AgentRunResult {
fn from(r: ca::RunResult) -> Self {
AgentRunResult {
run_id: r.run_id,
status: r.status.as_str().into(),
turn: r.turn,
text: r.text,
stop_reason: r.stop_reason,
usage_json: r.usage.map(|u| u.to_string()),
error: r.error,
tool_calls: r.tool_calls,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct AgentArtifact {
pub path: String,
pub size: u64,
pub modified_ms: u64,
}
#[derive(uniffi::Object)]
pub struct Agents {
inner: ca::Agents,
location: String,
}
impl Agents {
pub(crate) async fn over(
guest: cua_spacesd_client::SpacesdClient,
location: &str,
) -> Result<Arc<Agents>> {
let inner = run(async move { Ok(ca::Agents::new(guest).await?) }).await?;
Ok(Arc::new(Agents {
inner,
location: location.to_string(),
}))
}
}
fn entry() -> &'static str {
if cua_telemetry::global().product() == "cli" {
"cli"
} else {
"sdk"
}
}
fn observe(info: &ca::RunInfo) {
if let Some((outcome, variant)) = cua_telemetry::events::agent_run_end(
info.status.as_str(),
info.stop_reason.as_deref(),
info.error.as_deref(),
) {
cua_telemetry::global().agent_run_finished(&info.run_id, outcome, variant);
}
}
#[uniffi::export]
pub fn agent_harnesses() -> String {
let v: Vec<_> = ca::harness::HARNESSES.iter().map(|h| h.info()).collect();
serde_json::to_string_pretty(&v).unwrap_or_default()
}
fn attachments(files: Vec<AgentFile>) -> Vec<ca::Attachment> {
files
.into_iter()
.map(|f| ca::Attachment {
name: f.name,
bytes: f.bytes,
})
.collect()
}
fn run_options(o: AgentRunOptions) -> Result<ca::RunOptions> {
let mut env: std::collections::BTreeMap<String, String> = o.env.into_iter().collect();
if !o.env_from_host.is_empty() {
let allowed: Vec<&str> = ca::harness::HARNESSES
.iter()
.flat_map(|h| h.keys.iter().copied())
.chain(["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"])
.collect();
for n in o.env_from_host {
if !allowed.contains(&n.as_str()) {
return Err(CuaError::InvalidArgument(format!(
"{n} is not a provider key variable"
)));
}
let v = std::env::var(&n)
.ok()
.filter(|v| !v.is_empty())
.ok_or_else(|| CuaError::InvalidArgument(format!("{n} is not set")))?;
env.insert(n, v);
}
}
let wire = match o.wire.as_deref() {
None => None,
Some(w) => Some(
ca::Wire::parse(w)
.ok_or_else(|| CuaError::InvalidArgument(format!("unknown wire {w:?}")))?,
),
};
Ok(ca::RunOptions {
cwd: o.cwd,
repo: o.repo,
branch: o.branch,
files: attachments(o.files),
env,
mcp_servers: o
.mcp_servers
.into_iter()
.map(|m| ca::McpServer {
name: m.name,
url: m.url,
headers: m.headers.into_iter().collect(),
command: m.command,
args: m.args,
env: m.env.into_iter().collect(),
})
.collect(),
sandbox_mcp: o.sandbox_mcp,
skills: o.skills,
endpoint: o.base_url.map(|base_url| ca::Endpoint {
base_url,
wire,
model: o.model.clone(),
}),
model: o.model,
install: o.install,
exit_when_idle: o.exit_when_idle,
idle_exit_secs: None,
label: o.label,
home: None,
home_dir: None,
bridge: false,
})
}
#[uniffi::export]
impl Agents {
pub async fn run(
&self,
harness: String,
prompt: String,
options: Option<AgentRunOptions>,
) -> Result<Arc<AgentRun>> {
let inner = self.inner.clone();
let opts = match run_options(options.unwrap_or_default()) {
Ok(o) => o,
Err(e) => {
cua_telemetry::global().agent_run_start_failed(
&harness,
&self.location,
entry(),
Some(e.variant()),
);
return Err(e);
}
};
let h = harness.clone();
let started = run(async move { Ok(inner.start(&h, &prompt, opts).await?) }).await;
let started = match started {
Ok(s) => s,
Err(e) => {
cua_telemetry::global().agent_run_start_failed(
&harness,
&self.location,
entry(),
Some(e.variant()),
);
return Err(e);
}
};
cua_telemetry::global().agent_run_started(
&started.run_id,
&started.harness,
&self.location,
entry(),
);
Ok(Arc::new(AgentRun {
inner: self.inner.clone(),
run_id: started.run_id,
harness: started.harness,
}))
}
pub async fn get(&self, run_id: String) -> Result<Arc<AgentRun>> {
let inner = self.inner.clone();
let id = run_id.clone();
let info = run(async move { Ok(inner.status(&id).await?) }).await?;
observe(&info);
Ok(Arc::new(AgentRun {
inner: self.inner.clone(),
run_id,
harness: info.harness.unwrap_or_default(),
}))
}
pub async fn list(&self) -> Result<Vec<AgentRunInfo>> {
let inner = self.inner.clone();
run(async move {
let runs = inner.list().await?;
runs.iter().for_each(observe);
Ok(runs.into_iter().map(Into::into).collect())
})
.await
}
pub async fn ensure(&self, ids: Vec<String>) -> Result<Vec<String>> {
let inner = self.inner.clone();
run(async move {
let mut items: Vec<&str> = vec![];
for id in &ids {
match ca::harness::harness(id) {
Some(h) => items.extend(h.installs.iter().copied()),
None => items.push(id),
}
}
let progress = inner.ensure(&items, |_| {}).await?;
Ok(progress
.into_iter()
.map(|p| format!("{} {} {}", p.id, p.phase, p.detail))
.collect())
})
.await
}
}
#[derive(uniffi::Object)]
pub struct AgentRun {
inner: ca::Agents,
run_id: String,
harness: String,
}
#[uniffi::export]
impl AgentRun {
pub fn run_id(&self) -> String {
self.run_id.clone()
}
pub fn harness(&self) -> String {
self.harness.clone()
}
pub async fn status(&self) -> Result<AgentRunInfo> {
let (a, id) = (self.inner.clone(), self.run_id.clone());
run(async move {
let info = a.status(&id).await?;
observe(&info);
Ok(info.into())
})
.await
}
pub async fn events(&self, cursor: u64, max: Option<u32>) -> Result<AgentEventPage> {
let (a, id) = (self.inner.clone(), self.run_id.clone());
run(async move {
let p = a.events(&id, cursor, max.unwrap_or(200) as usize).await?;
Ok(AgentEventPage {
events: p.events.iter().map(Into::into).collect(),
cursor: p.cursor,
caught_up: p.caught_up,
})
})
.await
}
pub async fn send(&self, text: String, files: Option<Vec<AgentFile>>) -> Result<AgentRunInfo> {
let (a, id) = (self.inner.clone(), self.run_id.clone());
let files = attachments(files.unwrap_or_default());
run(async move { Ok(a.send(&id, &text, files).await?.into()) }).await
}
pub async fn interrupt(&self) -> Result<AgentRunInfo> {
let (a, id) = (self.inner.clone(), self.run_id.clone());
run(async move { Ok(a.interrupt(&id).await?.into()) }).await
}
pub async fn stop(&self) -> Result<AgentRunInfo> {
let (a, id) = (self.inner.clone(), self.run_id.clone());
run(async move {
if cua_telemetry::global().agent_run_pending(&id)
&& let Ok(info) = a.status(&id).await
{
observe(&info);
}
let info = a.stop(&id).await?;
cua_telemetry::global().agent_run_finished(
&id,
cua_telemetry::Outcome::Cancelled,
None,
);
Ok(info.into())
})
.await
}
pub async fn result(&self) -> Result<AgentRunResult> {
let (a, id) = (self.inner.clone(), self.run_id.clone());
run(async move {
let r = a.result(&id).await?;
if r.status != ca::RunStatus::Running
&& let Ok(info) = a.status(&id).await
{
observe(&info);
}
Ok(r.into())
})
.await
}
pub async fn wait(&self, timeout_ms: Option<u64>) -> Result<AgentRunResult> {
let (a, id) = (self.inner.clone(), self.run_id.clone());
let t = Duration::from_millis(timeout_ms.unwrap_or(30 * 60 * 1000));
run(async move {
let r = a.wait(&id, t).await?;
if let Ok(info) = a.status(&id).await {
observe(&info);
}
Ok(r.into())
})
.await
}
pub async fn artifacts(&self) -> Result<Vec<AgentArtifact>> {
let (a, id) = (self.inner.clone(), self.run_id.clone());
run(async move {
Ok(a.artifacts(&id)
.await?
.into_iter()
.map(|x| AgentArtifact {
path: x.path,
size: x.size,
modified_ms: x.modified_ms,
})
.collect())
})
.await
}
pub async fn remove(&self) -> Result<()> {
let (a, id) = (self.inner.clone(), self.run_id.clone());
run(async move { Ok(a.remove(&id).await?) }).await
}
}
#[uniffi::export]
impl SpacesdClient {
pub async fn agents(&self) -> Result<Arc<Agents>> {
Agents::over(self.client.clone(), &self.location).await
}
}