supercode-interchange 0.4.19

Canonical, provider-neutral session interchange primitives for Supercode
Documentation
//! The home and its profiles (§2.1): the root folder IS the `default`
//! profile; `profiles/<name>/` are the named ones.

use std::collections::BTreeMap;
use std::path::PathBuf;

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use super::{Access, ChannelConfig, Fire, Job, Obligation, Route, WebhookSubscription, WorkerSpec};
use crate::ontology::{Binding, SurfaceKey};

/// The persona: `AGENTS.md` at the profile root, content hash first.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PersonaRef {
    /// Always `AGENTS.md` in our folder (`SOUL.md` in a Hermes home).
    pub path: String,
    /// The text, when loaded.
    #[serde(default)]
    pub text: Option<String>,
    /// Hex sha256 of the text.
    pub sha256: String,
}

/// Which conversations share one session (Hermes `group_sessions_per_user`,
/// `thread_sessions_per_user`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum ExpiryScope {
    /// One session per chat.
    #[default]
    PerChat,
    /// One per user inside a group.
    PerUserInGroup,
    /// One per thread.
    PerThread,
}

/// When a conversation's session ends on its own (`docs/session-lifecycle.md`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExpiryPolicy {
    /// Idle minutes before the session ends; 1440 by default.
    #[serde(default = "default_idle_minutes")]
    pub idle_minutes: u32,
    /// Local hour of the daily reset; `None` = off.
    #[serde(default)]
    pub daily_reset_hour: Option<u8>,
    /// Session scope.
    #[serde(default)]
    pub scope: ExpiryScope,
}

fn default_idle_minutes() -> u32 {
    1440
}

impl Default for ExpiryPolicy {
    fn default() -> Self {
        Self {
            idle_minutes: 1440,
            daily_reset_hour: None,
            scope: ExpiryScope::PerChat,
        }
    }
}

/// What a profile carries that the model does not interpret.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ProfileResidue {
    /// `config.yaml` keys that are the source harness's, verbatim.
    #[serde(default)]
    pub config: BTreeMap<String, Value>,
    /// Unmodeled files, by path relative to the profile dir; copied verbatim.
    #[serde(default)]
    pub files: Vec<String>,
}

/// One profile: a folder with `AGENTS.md` at its root.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Profile {
    /// `default` for the root, else the folder name.
    pub name: String,
    /// The folder.
    pub dir: PathBuf,
    /// O — which harness runs it.
    #[serde(default)]
    pub worker: Option<WorkerSpec>,
    /// O — `AGENTS.md`.
    #[serde(default)]
    pub persona: Option<PersonaRef>,
    /// H — `config.yaml` `platforms:` blocks.
    #[serde(default)]
    pub channels: BTreeMap<String, ChannelConfig>,
    /// H — `gateway.profile_routes`.
    #[serde(default)]
    pub routes: Vec<Route>,
    /// H — idle / daily / scope.
    #[serde(default)]
    pub expiry: ExpiryPolicy,
    /// H — `/sethome`.
    #[serde(default)]
    pub home: Option<SurfaceKey>,
    /// H — `cron/jobs.json`.
    #[serde(default)]
    pub jobs: BTreeMap<String, Job>,
    /// H — `webhook_subscriptions.json`.
    #[serde(default)]
    pub subscriptions: BTreeMap<String, WebhookSubscription>,
    /// H — allowlists and pairing.
    #[serde(default)]
    pub access: Access,
    /// O — the conversations, keyed by their slot (the surface key string for
    /// a live one, `<key>#<started_at>[#n]` for ended ones).
    #[serde(default)]
    pub bindings: BTreeMap<String, Binding>,
    /// H — `cron/executions.db`.
    #[serde(default)]
    pub fires: Vec<Fire>,
    /// H — `delivery_obligations`.
    #[serde(default)]
    pub obligations: Vec<Obligation>,
    /// What the model does not interpret.
    #[serde(default)]
    pub residue: ProfileResidue,
}

/// The whole home: the persisted half of `OrchestratorState` (§2.1); the
/// runtime half is never written and is not part of the wire.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Orchestration {
    /// The home folder.
    pub root: PathBuf,
    /// `default` is the root folder; the rest are `profiles/<name>/`.
    pub profiles: BTreeMap<String, Profile>,
}