objectiveai-sdk 2.2.1

ObjectiveAI SDK, definitions, and utilities
Documentation
//! `agents tasks schedule` — register a command + interval (or
//! oneshot) in `tasks.sqlite`. Add-only leaf; the runner that
//! actually fires schedules is follow-up work tracked by #216.
//!
//! Schedule per row:
//! - `command`: argv vector to invoke on each scheduled poll.
//! - `interval_seconds`: `Some(n)` for a recurring schedule with
//!   `n` seconds as the floor between invocations; `None` for a
//!   **oneshot** that the runner fires once on the next poll and
//!   deletes the row. The CLI gates this via mutually-exclusive
//!   `--interval <humantime>` / `--oneshot` flags.
//! - The caller's full `AgentArguments` snapshot — captured by
//!   the CLI handler so the runner can re-install identity env
//!   vars at fire-time.

use crate::cli::command::CommandRequest;

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[schemars(rename = "cli.command.tasks.schedule.Request")]
pub struct Request {
    pub path_type: Path,
    /// User-facing identifier. Unique per agent instance hierarchy —
    /// a second `schedule` with the same `(name, aih)` fails the
    /// `schedules` UNIQUE constraint unless `overwrite` is set.
    /// `agents tasks run` tags every streamed output line with this
    /// name so the caller can attribute output to its source schedule.
    pub name: String,
    /// argv to invoke on each scheduled poll.
    pub command: Vec<String>,
    /// Human-readable label. Required — surfaces on every
    /// `agents tasks list` row, and the runner uses it in
    /// observability output.
    pub description: String,
    /// Floor on wall-clock seconds between invocations. `None`
    /// marks a **oneshot** schedule — the runner fires it once on
    /// the next poll and deletes the row. `Some(n)` is a recurring
    /// schedule with `n` seconds as the minimum gap between
    /// invocations.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[schemars(extend("omitempty" = true))]
    pub interval_seconds: Option<u64>,
    /// Shadow an existing `(name, agent_instance_hierarchy)` schedule
    /// instead of erroring on collision: a NEW row is inserted with
    /// `version = max + 1`. Older versions never list or run again but
    /// are kept so run history stays per-version; the new version has
    /// no runs yet, so it fires fresh.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub overwrite: bool,
    #[serde(flatten)]
    pub base: crate::cli::command::RequestBase,
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[schemars(rename = "cli.command.tasks.schedule.Path")]
pub enum Path {
    #[serde(rename = "tasks/schedule")]
    AgentsTasksSchedule,
}

impl CommandRequest for Request {
    fn into_command(&self) -> Vec<String> {
        let mut argv = vec![
            "tasks".to_string(),
            "schedule".to_string(),
        ];
        argv.push("--name".to_string());
        argv.push(self.name.clone());
        argv.push("--description".to_string());
        argv.push(self.description.clone());
        match self.interval_seconds {
            Some(secs) => {
                argv.push("--interval".to_string());
                // Round-trip as humantime — `Duration::from_secs(N)`
                // formats as e.g. `30s` / `1h30m` so the CLI
                // re-parses cleanly.
                argv.push(
                    humantime::format_duration(std::time::Duration::from_secs(secs))
                        .to_string(),
                );
            }
            None => argv.push("--oneshot".to_string()),
        }
        if self.overwrite {
            argv.push("--overwrite".to_string());
        }
        self.base.push_flags(&mut argv);
        // `--` separator so command argv that itself contains flags
        // round-trips cleanly through the trailing-var-arg parse.
        argv.push("--".to_string());
        argv.extend(self.command.iter().cloned());
        argv
    }

    fn request_base(&self) -> &crate::cli::command::RequestBase {
        &self.base
    }

    fn request_base_mut(&mut self) -> Option<&mut crate::cli::command::RequestBase> {
        Some(&mut self.base)
    }
}

/// The created schedule's user-facing identity: its `--name`, the
/// caller hierarchy it was registered under, and the version this call
/// minted (`1` on first creation, `max + 1` per `--overwrite` — each
/// version is its own row; older versions are shadowed but kept for
/// per-version run history).
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[schemars(rename = "cli.command.tasks.schedule.Response")]
pub struct Response {
    pub name: String,
    pub agent_instance_hierarchy: String,
    pub version: u64,
}

#[derive(clap::Args)]
#[command(group(
    clap::ArgGroup::new("schedule_kind")
        .required(true)
        .multiple(false)
        .args(["interval", "oneshot"])
))]
pub struct Args {
    /// Minimum interval between scheduled invocations. Humantime
    /// format — `30s`, `5m`, `1h30m`, `2d`. Treated as a floor,
    /// not a wall-clock deadline (#216). Mutually exclusive with
    /// `--oneshot`.
    #[arg(long)]
    pub interval: Option<String>,
    /// User-facing identifier. Globally unique. `agents tasks
    /// run` tags every streamed output line with this name.
    #[arg(long)]
    pub name: String,
    /// Human-readable label for this schedule. Required —
    /// surfaces on every `agents tasks list` row.
    #[arg(long)]
    pub description: String,
    /// Fire the command once on the next harness poll, then
    /// delete the row. Mutually exclusive with `--interval`.
    #[arg(long)]
    pub oneshot: bool,
    /// Shadow an existing `(name, agent-instance-hierarchy)` schedule
    /// instead of erroring on collision: inserts a NEW version
    /// (`max + 1`) that supersedes the old ones. Old versions keep
    /// their run history but never list or run again.
    #[arg(long)]
    pub overwrite: bool,
    #[command(flatten)]
    pub base: crate::cli::command::RequestBaseArgs,
    /// Command and arguments to run on each scheduled invocation.
    /// Pass after `--` so flags meant for the inner command don't
    /// collide with the leaf's own (`--interval` / `--oneshot` /
    /// `--jq`).
    #[arg(trailing_var_arg = true, allow_hyphen_values = true, num_args = 1..)]
    pub command: Vec<String>,
}

#[derive(clap::Args)]
#[command(args_conflicts_with_subcommands = true)]
pub struct Command {
    #[command(flatten)]
    pub args: Args,
    #[command(subcommand)]
    pub schema: Option<Schema>,
}

#[derive(clap::Subcommand)]
pub enum Schema {
    /// Emit the JSON Schema for this leaf's `Request` type and exit.
    RequestSchema(request_schema::Args),
    /// Emit the JSON Schema for this leaf's `Response` type and exit.
    ResponseSchema(response_schema::Args),
}

impl TryFrom<Args> for Request {
    type Error = crate::cli::command::FromArgsError;
    fn try_from(args: Args) -> Result<Self, Self::Error> {
        // The `schedule_kind` clap group guarantees exactly one
        // of `--interval` / `--oneshot` is present.
        let interval_seconds = match (args.interval, args.oneshot) {
            (Some(interval), false) => {
                let parsed =
                    humantime::parse_duration(&interval).map_err(|source| {
                        crate::cli::command::FromArgsError {
                            field: "interval",
                            source: source.to_string().into(),
                        }
                    })?;
                Some(parsed.as_secs())
            }
            (None, true) => None,
            _ => unreachable!(
                "clap group `schedule_kind` enforces exactly one of `--interval` | `--oneshot`"
            ),
        };
        if args.command.is_empty() {
            return Err(crate::cli::command::FromArgsError {
                field: "command",
                source: "schedule requires at least one positional argument (the command)"
                    .to_string()
                    .into(),
            });
        }
        Ok(Self {
            path_type: Path::AgentsTasksSchedule,
            name: args.name,
            command: args.command,
            description: args.description,
            interval_seconds,
            overwrite: args.overwrite,
            base: args.base.into(),
        })
    }
}

#[cfg(feature = "cli-executor")]
pub async fn execute<E: crate::cli::command::CommandExecutor>(
    executor: &E,
    mut request: Request,
    agent_arguments: Option<&crate::cli::command::AgentArguments>,
) -> Result<Response, E::Error> {
    request.base.clear_transform();
    executor.execute_one(request, agent_arguments).await
}

#[cfg(feature = "cli-executor")]
pub async fn execute_transform<E: crate::cli::command::CommandExecutor>(
    executor: &E,
    mut request: Request,
    transform: crate::cli::command::Transform,
    agent_arguments: Option<&crate::cli::command::AgentArguments>,
) -> Result<serde_json::Value, E::Error> {
    request.base.set_transform(transform);
    executor.execute_one(request, agent_arguments).await
}

#[cfg(feature = "mcp")]
impl crate::cli::command::CommandResponse for Response {
    fn into_mcp(self) -> crate::cli::command::McpResponseItem {
        crate::cli::command::McpResponseItem::JSONL(serde_json::to_value(self).unwrap())
    }
}

pub mod request_schema;

pub mod response_schema;