Add automation app-server API

This commit is contained in:
Andrei Eternal
2026-06-16 17:13:31 -07:00
parent 47d3ad7d8b
commit 5f17fb5cfe
13 changed files with 626 additions and 0 deletions
@@ -170,6 +170,61 @@
}
]
},
"AutomationStatus": {
"enum": [
"ACTIVE",
"PAUSED"
],
"type": "string"
},
"AutomationTarget": {
"description": "EXPERIMENTAL - typed automation target for recurring scheduled work.",
"oneOf": [
{
"properties": {
"cwds": {
"items": {
"type": "string"
},
"type": "array"
},
"type": {
"enum": [
"cron"
],
"title": "CronAutomationTargetType",
"type": "string"
}
},
"required": [
"cwds",
"type"
],
"title": "CronAutomationTarget",
"type": "object"
},
{
"properties": {
"threadId": {
"type": "string"
},
"type": {
"enum": [
"heartbeat"
],
"title": "HeartbeatAutomationTargetType",
"type": "string"
}
},
"required": [
"threadId",
"type"
],
"title": "HeartbeatAutomationTarget",
"type": "object"
}
]
},
"ByteRange": {
"properties": {
"end": {
@@ -6893,6 +6893,133 @@
],
"type": "string"
},
"Automation": {
"description": "EXPERIMENTAL - durable recurring automation or heartbeat.",
"properties": {
"createdAt": {
"format": "int64",
"type": "integer"
},
"id": {
"type": "string"
},
"lastRunAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"model": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"nextRunAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"prompt": {
"type": "string"
},
"reasoningEffort": {
"anyOf": [
{
"$ref": "#/definitions/v2/ReasoningEffort"
},
{
"type": "null"
}
]
},
"rrule": {
"type": "string"
},
"status": {
"$ref": "#/definitions/v2/AutomationStatus"
},
"target": {
"$ref": "#/definitions/v2/AutomationTarget"
},
"updatedAt": {
"format": "int64",
"type": "integer"
}
},
"required": [
"createdAt",
"id",
"name",
"prompt",
"rrule",
"status",
"target",
"updatedAt"
],
"type": "object"
},
"AutomationStatus": {
"enum": [
"ACTIVE",
"PAUSED"
],
"type": "string"
},
"AutomationTarget": {
"description": "EXPERIMENTAL - typed automation target for recurring scheduled work.",
"oneOf": [
{
"properties": {
"cwds": {
"items": {
"type": "string"
},
"type": "array"
},
"type": {
"enum": [
"cron"
],
"title": "CronAutomationTargetType",
"type": "string"
}
},
"required": [
"cwds",
"type"
],
"title": "CronAutomationTarget",
"type": "object"
},
{
"properties": {
"threadId": {
"type": "string"
},
"type": {
"enum": [
"heartbeat"
],
"title": "HeartbeatAutomationTargetType",
"type": "string"
}
},
"required": [
"threadId",
"type"
],
"title": "HeartbeatAutomationTarget",
"type": "object"
}
]
},
"ByteRange": {
"properties": {
"end": {
@@ -1095,6 +1095,133 @@
],
"type": "string"
},
"Automation": {
"description": "EXPERIMENTAL - durable recurring automation or heartbeat.",
"properties": {
"createdAt": {
"format": "int64",
"type": "integer"
},
"id": {
"type": "string"
},
"lastRunAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"model": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"nextRunAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"prompt": {
"type": "string"
},
"reasoningEffort": {
"anyOf": [
{
"$ref": "#/definitions/ReasoningEffort"
},
{
"type": "null"
}
]
},
"rrule": {
"type": "string"
},
"status": {
"$ref": "#/definitions/AutomationStatus"
},
"target": {
"$ref": "#/definitions/AutomationTarget"
},
"updatedAt": {
"format": "int64",
"type": "integer"
}
},
"required": [
"createdAt",
"id",
"name",
"prompt",
"rrule",
"status",
"target",
"updatedAt"
],
"type": "object"
},
"AutomationStatus": {
"enum": [
"ACTIVE",
"PAUSED"
],
"type": "string"
},
"AutomationTarget": {
"description": "EXPERIMENTAL - typed automation target for recurring scheduled work.",
"oneOf": [
{
"properties": {
"cwds": {
"items": {
"type": "string"
},
"type": "array"
},
"type": {
"enum": [
"cron"
],
"title": "CronAutomationTargetType",
"type": "string"
}
},
"required": [
"cwds",
"type"
],
"title": "CronAutomationTarget",
"type": "object"
},
{
"properties": {
"threadId": {
"type": "string"
},
"type": {
"enum": [
"heartbeat"
],
"title": "HeartbeatAutomationTargetType",
"type": "string"
}
},
"required": [
"threadId",
"type"
],
"title": "HeartbeatAutomationTarget",
"type": "object"
}
]
},
"ByteRange": {
"properties": {
"end": {
@@ -0,0 +1,11 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ReasoningEffort } from "../ReasoningEffort";
import type { AutomationStatus } from "./AutomationStatus";
import type { AutomationTarget } from "./AutomationTarget";
/**
* EXPERIMENTAL - durable recurring automation or heartbeat.
*/
export type Automation = { id: string, name: string, prompt: string, status: AutomationStatus, rrule: string, nextRunAt: bigint | null, lastRunAt: bigint | null, createdAt: bigint, updatedAt: bigint, model: string | null, reasoningEffort: ReasoningEffort | null, target: AutomationTarget, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AutomationStatus = "ACTIVE" | "PAUSED";
@@ -0,0 +1,8 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* EXPERIMENTAL - typed automation target for recurring scheduled work.
*/
export type AutomationTarget = { "type": "cron", cwds: Array<string>, } | { "type": "heartbeat", threadId: string, };
@@ -36,6 +36,9 @@ export type { AskForApproval } from "./AskForApproval";
export type { AttestationGenerateParams } from "./AttestationGenerateParams";
export type { AttestationGenerateResponse } from "./AttestationGenerateResponse";
export type { AutoReviewDecisionSource } from "./AutoReviewDecisionSource";
export type { Automation } from "./Automation";
export type { AutomationStatus } from "./AutomationStatus";
export type { AutomationTarget } from "./AutomationTarget";
export type { ByteRange } from "./ByteRange";
export type { CancelLoginAccountParams } from "./CancelLoginAccountParams";
export type { CancelLoginAccountResponse } from "./CancelLoginAccountResponse";
@@ -618,6 +618,42 @@ client_request_definitions! {
serialization: None,
response: v2::ThreadListResponse,
},
#[experimental("automation/list")]
AutomationList => "automation/list" {
params: v2::AutomationListParams,
serialization: global_shared_read("automation"),
response: v2::AutomationListResponse,
},
#[experimental("automation/read")]
AutomationRead => "automation/read" {
params: v2::AutomationReadParams,
serialization: global_shared_read("automation"),
response: v2::AutomationReadResponse,
},
#[experimental("automation/create")]
AutomationCreate => "automation/create" {
params: v2::AutomationCreateParams,
serialization: global("automation"),
response: v2::AutomationCreateResponse,
},
#[experimental("automation/update")]
AutomationUpdate => "automation/update" {
params: v2::AutomationUpdateParams,
serialization: global("automation"),
response: v2::AutomationUpdateResponse,
},
#[experimental("automation/delete")]
AutomationDelete => "automation/delete" {
params: v2::AutomationDeleteParams,
serialization: global("automation"),
response: v2::AutomationDeleteResponse,
},
#[experimental("automation/runNow")]
AutomationRunNow => "automation/runNow" {
params: v2::AutomationRunNowParams,
serialization: global("automation"),
response: v2::AutomationRunNowResponse,
},
#[experimental("thread/search")]
ThreadSearch => "thread/search" {
params: v2::ThreadSearchParams,
@@ -0,0 +1,197 @@
use codex_protocol::openai_models::ReasoningEffort;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf;
use ts_rs::TS;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase", tag = "type")]
#[ts(export_to = "v2/", rename_all = "camelCase", tag = "type")]
/// EXPERIMENTAL - typed automation target for recurring scheduled work.
pub enum AutomationTarget {
Cron {
cwds: Vec<PathBuf>,
},
Heartbeat {
#[serde(rename = "threadId")]
#[ts(rename = "threadId")]
thread_id: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[ts(export_to = "v2/")]
pub enum AutomationStatus {
#[serde(rename = "ACTIVE")]
#[ts(rename = "ACTIVE")]
Active,
#[serde(rename = "PAUSED")]
#[ts(rename = "PAUSED")]
Paused,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL - durable recurring automation or heartbeat.
pub struct Automation {
pub id: String,
pub name: String,
pub prompt: String,
pub status: AutomationStatus,
pub rrule: String,
pub next_run_at: Option<i64>,
pub last_run_at: Option<i64>,
pub created_at: i64,
pub updated_at: i64,
pub model: Option<String>,
pub reasoning_effort: Option<ReasoningEffort>,
pub target: AutomationTarget,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL - list recurring automations.
pub struct AutomationListParams {
/// Opaque pagination cursor returned by a previous call.
#[ts(optional = nullable)]
pub cursor: Option<String>,
/// Optional page size; defaults to a reasonable server-side value.
#[ts(optional = nullable)]
pub limit: Option<u32>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL - paginated automation list response.
pub struct AutomationListResponse {
pub data: Vec<Automation>,
pub next_cursor: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL - read one automation by id.
pub struct AutomationReadParams {
pub automation_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AutomationReadResponse {
pub automation: Option<Automation>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL - create a recurring automation or heartbeat.
pub struct AutomationCreateParams {
pub name: String,
pub prompt: String,
#[ts(optional = nullable)]
pub rrule: Option<String>,
#[ts(optional = nullable)]
pub model: Option<String>,
#[ts(optional = nullable)]
pub reasoning_effort: Option<ReasoningEffort>,
#[ts(optional = nullable)]
pub status: Option<AutomationStatus>,
pub target: AutomationTarget,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AutomationCreateResponse {
pub automation: Automation,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL - update an existing automation.
pub struct AutomationUpdateParams {
pub automation_id: String,
#[ts(optional = nullable)]
pub name: Option<String>,
#[ts(optional = nullable)]
pub prompt: Option<String>,
#[serde(
default,
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub rrule: Option<Option<String>>,
#[serde(
default,
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub model: Option<Option<String>>,
#[serde(
default,
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option",
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
skip_serializing_if = "Option::is_none"
)]
#[ts(optional = nullable)]
pub reasoning_effort: Option<Option<ReasoningEffort>>,
#[ts(optional = nullable)]
pub status: Option<AutomationStatus>,
#[ts(optional = nullable)]
pub target: Option<AutomationTarget>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AutomationUpdateResponse {
pub automation: Option<Automation>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL - delete an automation by id.
pub struct AutomationDeleteParams {
pub automation_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AutomationDeleteResponse {
pub deleted: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL - start an immediate run for an automation.
pub struct AutomationRunNowParams {
pub automation_id: String,
#[ts(optional = nullable)]
pub thread_id: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AutomationRunNowResponse {
pub found: bool,
pub started_count: u32,
}
#[cfg(test)]
#[path = "automation_tests.rs"]
mod tests;
@@ -0,0 +1,26 @@
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn automation_update_omits_absent_clearable_fields() {
let params = AutomationUpdateParams {
automation_id: "automation-1".to_string(),
name: None,
prompt: None,
rrule: None,
model: None,
reasoning_effort: None,
status: None,
target: None,
};
let serialized = serde_json::to_value(params).expect("serialize automation update");
assert_eq!(
serialized["automationId"],
serde_json::json!("automation-1")
);
assert!(serialized.get("rrule").is_none());
assert!(serialized.get("model").is_none());
assert!(serialized.get("reasoningEffort").is_none());
}
@@ -3,6 +3,7 @@ mod shared;
mod account;
mod apps;
mod attestation;
mod automation;
mod collaboration_mode;
mod command_exec;
mod config;
@@ -29,6 +30,7 @@ mod windows_sandbox;
pub use account::*;
pub use apps::*;
pub use attestation::*;
pub use automation::*;
pub use collaboration_mode::*;
pub use command_exec::*;
pub use config::*;
+6
View File
@@ -148,6 +148,12 @@ Example with notification opt-out:
- `thread/goal/clear` — clear the current persisted goal for a materialized thread; returns whether a goal was removed and emits `thread/goal/cleared` when state changes.
- `thread/goal/updated` — notification emitted whenever a thread goal changes; includes the full current goal.
- `thread/goal/cleared` — notification emitted whenever a thread goal is removed.
- `automation/list` — experimental; list recurring automations visible from the initialized client, with cursor pagination.
- `automation/read` — experimental; read one automation by id; returns `automation: null` when it is missing or not visible.
- `automation/create` — experimental; create a recurring cron automation or heartbeat. Cron targets provide `cwds` and spawn a new thread per cwd when dispatched. Heartbeat targets provide `threadId` and deliver to that existing thread.
- `automation/update` — experimental; update an automation's name, prompt, schedule, status, model, reasoning effort, or target when the target remains valid for the caller.
- `automation/delete` — experimental; delete an automation by id; returns `deleted: false` when it is missing or not visible.
- `automation/runNow` — experimental; immediately dispatch an automation once. Returns `{ found, startedCount }`; `found: true, startedCount: 0` means another app-server already owns an in-flight run.
- `thread/settings/updated` — experimental notification emitted to subscribed clients when a loaded thread’s effective next-turn settings change; includes `threadId` and the full `threadSettings`.
- `thread/status/changed` — notification emitted when a loaded thread’s status changes (`threadId` + new `status`).
- `thread/archive` — move a thread’s rollout file into the archived directory and attempt to move any spawned descendant thread rollout files; returns `{}` on success and emits `thread/archived` for each archived thread.
@@ -8,6 +8,7 @@ use crate::attestation::app_server_attestation_provider;
use crate::config_manager::ConfigManager;
use crate::connection_rpc_gate::ConnectionRpcGate;
use crate::error_code::invalid_request;
use crate::error_code::method_not_found;
use crate::extensions::ThreadExtensionDependencies;
use crate::extensions::app_server_extension_event_sink;
use crate::extensions::guardian_agent_spawner;
@@ -1183,6 +1184,24 @@ impl MessageProcessor {
ClientRequest::ThreadList { params, .. } => {
self.thread_processor.thread_list(params).await
}
ClientRequest::AutomationList { .. } => {
Err(automation_api_not_implemented("automation/list"))
}
ClientRequest::AutomationRead { .. } => {
Err(automation_api_not_implemented("automation/read"))
}
ClientRequest::AutomationCreate { .. } => {
Err(automation_api_not_implemented("automation/create"))
}
ClientRequest::AutomationUpdate { .. } => {
Err(automation_api_not_implemented("automation/update"))
}
ClientRequest::AutomationDelete { .. } => {
Err(automation_api_not_implemented("automation/delete"))
}
ClientRequest::AutomationRunNow { .. } => {
Err(automation_api_not_implemented("automation/runNow"))
}
ClientRequest::ThreadSearch { params, .. } => {
self.thread_processor.thread_search(params).await
}
@@ -1485,6 +1504,10 @@ impl MessageProcessor {
}
}
fn automation_api_not_implemented(method: &str) -> JSONRPCErrorError {
method_not_found(format!("{method} is not supported yet"))
}
#[cfg(test)]
#[path = "message_processor_tracing_tests.rs"]
mod message_processor_tracing_tests;