cua-sdk 0.7.0

The cua SDK: the MIT #[uniffi::export] crate. One Cua object (embedded or connected to `cua daemon`) exposing env, sandboxes, local and spaces modules to Python, TypeScript, Swift and Kotlin.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
//! Persistent agents, routines, notifications and per-agent computer
//! access on [`Spaces`] (the Spaces `persistent-agents` tools, in process or
//! in the daemon).

use serde::Deserialize;
use serde_json::{Value, json};

use super::run;
use super::spaces::{Spaces, parse};
use crate::Result;

/// A persistent agent: a named harness whose memory (its home in the Cua
/// Drive) outlives its runs and its Space.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, uniffi::Record)]
pub struct PersistentAgentInfo {
    pub name: String,
    /// Harness id.
    pub harness: String,
    /// The Space it works in.
    pub space: String,
    #[serde(default)]
    pub model: Option<String>,
    #[serde(default)]
    pub base_url: Option<String>,
    #[serde(default)]
    pub env_from_host: Vec<String>,
    #[serde(default)]
    pub paused: bool,
    /// `running`, `suspended` (a paused local Space) or `released` (a
    /// paused cloud Space).
    #[serde(default)]
    pub space_state: String,
    /// The current run.
    #[serde(default)]
    pub run_id: Option<String>,
    /// Unix ms of the last home save.
    #[serde(default)]
    pub saved_ms: u64,
    #[serde(default)]
    pub created_ms: u64,
    #[serde(default)]
    pub last_error: Option<String>,
}

/// How to create a persistent agent.
#[derive(Debug, Clone, PartialEq, Eq, Default, uniffi::Record)]
pub struct PersistentAgentOptions {
    #[uniffi(default = None)]
    pub model: Option<String>,
    #[uniffi(default = None)]
    pub base_url: Option<String>,
    /// Provider key variables forwarded from the host's environment at each
    /// start.
    #[uniffi(default)]
    pub env_from_host: Vec<String>,
    /// More environment for every run (not secrets).
    #[uniffi(default)]
    pub env: std::collections::HashMap<String, String>,
}

/// What moved between the drive and a Space.
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, uniffi::Record)]
pub struct HomeTransfer {
    #[serde(default)]
    pub files: u64,
    #[serde(default)]
    pub bytes: u64,
    #[serde(default)]
    pub unchanged: u64,
    #[serde(default)]
    pub removed: u64,
    /// `path (kind)` of each file the secret scanner kept out.
    #[serde(default, deserialize_with = "blocked")]
    pub blocked: Vec<String>,
    #[serde(default)]
    pub millis: u64,
}

fn blocked<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Vec<String>, D::Error> {
    let pairs: Vec<(String, String)> = Deserialize::deserialize(d)?;
    Ok(pairs
        .into_iter()
        .map(|(p, k)| format!("{p} ({k})"))
        .collect())
}

/// What [`Spaces::persistent_agent_send`] did.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, uniffi::Record)]
pub struct AgentDelivery {
    pub run_id: String,
    /// A new run started (with the home restored) rather than a follow-up.
    pub started: bool,
    #[serde(default)]
    pub restored: Option<HomeTransfer>,
}

/// What [`Spaces::agent_pause`] did.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, uniffi::Record)]
pub struct AgentPauseReport {
    #[serde(default)]
    pub stopped_run: Option<String>,
    #[serde(default)]
    pub saved: Option<HomeTransfer>,
    pub space_state: String,
    #[serde(default)]
    pub millis: u64,
}

/// What [`Spaces::agent_resume`] did.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, uniffi::Record)]
pub struct AgentResumeReport {
    pub space: String,
    #[serde(default)]
    pub recreated: bool,
    #[serde(default)]
    pub restored: Option<HomeTransfer>,
    #[serde(default)]
    pub run_id: Option<String>,
    /// From the call to the home being back in a ready Space.
    #[serde(default)]
    pub ready_ms: u64,
}

/// A routine: a recurring turn of a persistent agent, fired by the daemon.
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct RoutineInfo {
    pub id: String,
    pub agent: String,
    pub title: String,
    pub prompt: String,
    /// `Every day at 8:00 AM`.
    pub label: String,
    pub enabled: bool,
    /// RFC 3339.
    pub next_fire: Option<String>,
    pub last_fired_at: Option<String>,
    pub last_outcome: Option<String>,
    pub last_run_id: Option<String>,
}

impl RoutineInfo {
    fn from_json(v: &Value) -> RoutineInfo {
        let s = |k: &str| v.get(k).and_then(Value::as_str).map(str::to_string);
        RoutineInfo {
            id: s("id").unwrap_or_default(),
            agent: s("botID").unwrap_or_default(),
            title: s("title").unwrap_or_default(),
            prompt: s("prompt").unwrap_or_default(),
            label: s("label").unwrap_or_default(),
            enabled: v.get("isEnabled").and_then(Value::as_bool).unwrap_or(true),
            next_fire: s("next_fire"),
            last_fired_at: s("lastFiredAt"),
            last_outcome: s("lastOutcome"),
            last_run_id: s("lastRunID"),
        }
    }
}

/// One entry of the notifications feed.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, uniffi::Record)]
pub struct NotificationInfo {
    pub id: String,
    pub at_ms: u64,
    #[serde(default)]
    pub agent: Option<String>,
    /// `turn_ended`, `message`, `approval` or `error`.
    pub kind: String,
    pub title: String,
    #[serde(default)]
    pub body: String,
    #[serde(default)]
    pub run_id: Option<String>,
    #[serde(default)]
    pub space: Option<String>,
    #[serde(default)]
    pub read: bool,
}

/// A persistent agent's grant on one of the user's computers.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, uniffi::Record)]
pub struct ComputerGrant {
    pub id: String,
    pub agent: String,
    pub machine: String,
    pub created_ms: u64,
    #[serde(default)]
    pub expires_ms: Option<u64>,
    #[serde(default)]
    pub revoked: bool,
}

fn list<T: serde::de::DeserializeOwned>(v: &Value, key: &str) -> Result<Vec<T>> {
    parse(v.get(key).cloned().unwrap_or(json!([])))
}

#[uniffi::export]
impl Spaces {
    /// Creates a persistent agent: `harness` working in `space`, its home at
    /// `agents/<name>/` in the Cua Volume.
    pub async fn persistent_agent_create(
        &self,
        name: String,
        harness: String,
        space: String,
        options: Option<PersistentAgentOptions>,
    ) -> Result<PersistentAgentInfo> {
        let host = self.tool_caller();
        let o = options.unwrap_or_default();
        run(async move {
            parse(
                host.tool_value(
                    "persistent_agent_create",
                    json!({"name": name, "agent": harness, "space": space, "model": o.model,
                           "base_url": o.base_url, "env_from_host": o.env_from_host, "env": o.env}),
                )
                .await?,
            )
        })
        .await
    }

    /// Every persistent agent.
    pub async fn persistent_agents(&self) -> Result<Vec<PersistentAgentInfo>> {
        let host = self.tool_caller();
        run(async move {
            list(
                &host.tool_value("persistent_agent_list", json!({})).await?,
                "agents",
            )
        })
        .await
    }

    /// Forgets a persistent agent (its home stays in the drive).
    pub async fn persistent_agent_remove(&self, name: String) -> Result<PersistentAgentInfo> {
        let host = self.tool_caller();
        run(async move {
            let v = host
                .tool_value("persistent_agent_remove", json!({"name": name}))
                .await?;
            parse(v.get("removed").cloned().unwrap_or(Value::Null))
        })
        .await
    }

    /// Gives a persistent agent a turn: a follow-up to its idle run, or a
    /// new run with its home restored.
    pub async fn persistent_agent_send(&self, name: String, text: String) -> Result<AgentDelivery> {
        let host = self.tool_caller();
        run(async move {
            parse(
                host.tool_value("persistent_agent_send", json!({"name": name, "text": text}))
                    .await?,
            )
        })
        .await
    }

    /// Saves a persistent agent's home into the drive now.
    pub async fn persistent_agent_save(&self, name: String) -> Result<HomeTransfer> {
        let host = self.tool_caller();
        run(async move {
            parse(
                host.tool_value("persistent_agent_save", json!({"name": name}))
                    .await?,
            )
        })
        .await
    }

    /// Pauses a persistent agent: run, routines and (local) Space.
    pub async fn agent_pause(&self, name: String) -> Result<AgentPauseReport> {
        let host = self.tool_caller();
        run(async move {
            parse(
                host.tool_value("agent_pause", json!({"name": name}))
                    .await?,
            )
        })
        .await
    }

    /// Resumes a paused persistent agent; with `prompt`, starts a run.
    pub async fn agent_resume(
        &self,
        name: String,
        prompt: Option<String>,
    ) -> Result<AgentResumeReport> {
        let host = self.tool_caller();
        run(async move {
            parse(
                host.tool_value("agent_resume", json!({"name": name, "prompt": prompt}))
                    .await?,
            )
        })
        .await
    }

    /// Adds a routine. Give exactly one of `every_minutes`, `daily_at`
    /// (`HH:MM`) or `weekly_on` (`mon 09:00`).
    pub async fn routine_add(
        &self,
        agent: String,
        title: String,
        prompt: String,
        every_minutes: Option<i64>,
        daily_at: Option<String>,
        weekly_on: Option<String>,
    ) -> Result<RoutineInfo> {
        let host = self.tool_caller();
        run(async move {
            let v = host
                .tool_value(
                    "routine_add",
                    json!({"agent": agent, "title": title, "prompt": prompt,
                           "every_minutes": every_minutes, "daily_at": daily_at,
                           "weekly_on": weekly_on}),
                )
                .await?;
            Ok(RoutineInfo::from_json(&v))
        })
        .await
    }

    /// Routines (of one agent, or all).
    pub async fn routines(&self, agent: Option<String>) -> Result<Vec<RoutineInfo>> {
        let host = self.tool_caller();
        run(async move {
            let v = host
                .tool_value("routine_list", json!({"agent": agent}))
                .await?;
            Ok(v.get("routines")
                .and_then(Value::as_array)
                .map(|a| a.iter().map(RoutineInfo::from_json).collect())
                .unwrap_or_default())
        })
        .await
    }

    /// Deletes a routine.
    pub async fn routine_remove(&self, id: String) -> Result<()> {
        let host = self.tool_caller();
        run(async move {
            host.tool_value("routine_remove", json!({"id": id})).await?;
            Ok(())
        })
        .await
    }

    /// Turns a routine on or off.
    pub async fn routine_set_enabled(&self, id: String, enabled: bool) -> Result<RoutineInfo> {
        let host = self.tool_caller();
        run(async move {
            let v = host
                .tool_value("routine_set_enabled", json!({"id": id, "enabled": enabled}))
                .await?;
            Ok(RoutineInfo::from_json(&v))
        })
        .await
    }

    /// Posts a notification to the Cua app. Returns its id.
    pub async fn notify_user(
        &self,
        title: String,
        body: Option<String>,
        agent: Option<String>,
    ) -> Result<String> {
        let host = self.tool_caller();
        run(async move {
            let v = host
                .tool_value(
                    "notify_user",
                    json!({"title": title, "body": body, "agent": agent}),
                )
                .await?;
            Ok(v.get("id")
                .and_then(Value::as_str)
                .unwrap_or_default()
                .to_string())
        })
        .await
    }

    /// The notifications feed, newest first.
    pub async fn notifications(
        &self,
        unread_only: bool,
        since_ms: Option<u64>,
    ) -> Result<Vec<NotificationInfo>> {
        let host = self.tool_caller();
        run(async move {
            list(
                &host
                    .tool_value(
                        "notifications_list",
                        json!({"unread_only": unread_only, "since_ms": since_ms}),
                    )
                    .await?,
                "notifications",
            )
        })
        .await
    }

    /// Marks notifications read (every one when `ids` is empty).
    pub async fn notifications_ack(&self, ids: Vec<String>) -> Result<u64> {
        let host = self.tool_caller();
        run(async move {
            let v = host
                .tool_value("notifications_ack", json!({"ids": ids}))
                .await?;
            Ok(v.get("marked").and_then(Value::as_u64).unwrap_or(0))
        })
        .await
    }

    /// Lets one persistent agent use one of the user's computers (asks for
    /// presence).
    pub async fn computer_access_grant(
        &self,
        agent: String,
        machine: String,
        expires_in_secs: Option<u64>,
    ) -> Result<ComputerGrant> {
        let host = self.tool_caller();
        run(async move {
            parse(
                host.tool_value(
                    "computer_access_grant",
                    json!({"agent": agent, "machine": machine, "expires_in_secs": expires_in_secs}),
                )
                .await?,
            )
        })
        .await
    }

    /// Takes a persistent agent's computer access back (every machine when
    /// `machine` is empty). Returns how many grants.
    pub async fn computer_access_revoke(
        &self,
        agent: String,
        machine: Option<String>,
    ) -> Result<u64> {
        let host = self.tool_caller();
        run(async move {
            let v = host
                .tool_value(
                    "computer_access_revoke",
                    json!({"agent": agent, "machine": machine}),
                )
                .await?;
            Ok(v.get("revoked").and_then(Value::as_u64).unwrap_or(0))
        })
        .await
    }

    /// Per-agent computer grants (of one agent, or all).
    pub async fn computer_access(&self, agent: Option<String>) -> Result<Vec<ComputerGrant>> {
        let host = self.tool_caller();
        run(async move {
            list(
                &host
                    .tool_value("computer_access_list", json!({"agent": agent}))
                    .await?,
                "grants",
            )
        })
        .await
    }
}