supercode-interchange 0.5.112

Canonical, provider-neutral session interchange primitives for Volter Harness
Documentation
//! The agent layer (mirrored ontology C8, `docs/architecture/orchestrator.md` ยง2.10): declared
//! agents, conversations separate from sessions (G5, as amended by M3), transfers and handoffs
//! (G2), usage and budgets (G3). Bindings name their conversation and agent (G1); routes name an
//! agent. Agent identity itself (ids, sponsor, lineage) lives with Teams and the orchestrator's
//! agent store; these records are the home's intent and its conversations' record.

use std::collections::BTreeMap;

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

use crate::ontology::SurfaceKey;

/// One agent this home declares: who answers, and which profile is its template.
/// A named profile implies its own agent of the same name; a declaration names others (several
/// agents may share one profile) and the parent each is declared under.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AgentDecl {
    /// Unique among the home's agents; a route, a speaker policy and a binding name it.
    pub name: String,
    /// The profile it runs as (its layered template).
    pub profile: String,
    /// A display label, not identity.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    /// The agent it is declared under, if not the home's root.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent: Option<String>,
    /// A person set on purpose as its sponsor; absent, the sponsor is inherited.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sponsor: Option<String>,
    /// What a source format said about it that the IR has no field for, verbatim.
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub residue: BTreeMap<String, Value>,
}

/// `thread`: a root and its replies, wherever it is shown; `private`: a harness session's own input.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ConversationKind {
    /// A root and its replies (a Room conversation, a chat thread, a mailbox exchange).
    Thread,
    /// A harness session's own conversation with its owner; its history is that transcript.
    Private,
}

/// How a participant is addressed on a thread (amendment M3).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ParticipantRole {
    /// Addressed: expected to act.
    To,
    /// Copied: receives every reply.
    Cc,
}

/// A person or an agent in a conversation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Participant {
    /// An agent's name, or a person as `<platform>:<user id>` (an identity observation).
    pub principal: String,
    /// To or CC.
    pub role: ParticipantRole,
    /// RFC3339.
    pub since: String,
    /// RFC3339, when it left.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub until: Option<String>,
}

/// Which bound agents receive a line on a shared surface (G1).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SpeakerPolicy {
    /// Mentions pick the agent; `default` takes the rest (a Hermes or OpenClaw surface: one agent).
    Addressed {
        /// The agent that takes an unaddressed line.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        default: Option<String>,
    },
    /// Every bound agent gets every line.
    All,
    /// Agents take lines in this order.
    RoundRobin {
        /// The order.
        order: Vec<String>,
    },
    /// An agent picks the next speaker (an event out and back, never a hidden model call).
    Selector {
        /// The selecting agent.
        selector: String,
    },
    /// The meeting's adapter decides (RH2's floor, the voice pack).
    Floor,
}

impl Default for SpeakerPolicy {
    fn default() -> Self {
        Self::Addressed { default: None }
    }
}

/// Where a conversation's record lives.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum HistoryRef {
    /// The platform's own thread (a chat thread, a Room conversation).
    Platform,
    /// The mailbox thread.
    Mailbox {
        /// The thread id.
        thread: String,
    },
    /// A private conversation's history: its owning session's transcript.
    Transcript {
        /// The session (`<harness>:<id>`).
        session: String,
    },
}

impl Default for HistoryRef {
    fn default() -> Self {
        Self::Platform
    }
}

/// A conversation, a Resource separate from the sessions bound to it (G5, amended by M3).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Conversation {
    /// Its id.
    pub id: String,
    /// Thread or private.
    pub kind: ConversationKind,
    /// Where a thread is shown; no profile is part of it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub surface: Option<SurfaceKey>,
    /// People and agents in it.
    #[serde(default)]
    pub participants: Vec<Participant>,
    /// Who receives a line.
    #[serde(default)]
    pub speaker: SpeakerPolicy,
    /// Where its record lives.
    #[serde(default)]
    pub history: HistoryRef,
    /// The agent a completed transfer gave the conversation to; an unaddressed line goes to it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub holder: Option<String>,
    /// The agent that took the last line (round robin's position).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_speaker: Option<String>,
}

/// How much of the conversation a transfer's receiver is given.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TransferHistory {
    /// All of it (the default).
    Full,
    /// What the filter keeps.
    Filtered,
    /// None.
    None,
}

/// A transfer's progress.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TransferState {
    /// Asked, not yet applied.
    Pending,
    /// The receiver holds the conversation.
    Done,
    /// The receiver may not take part.
    Refused,
}

/// One agent handing a conversation to another (OpenAI handoff, AutoGen Swarm, voice `hand_off`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Transfer {
    /// The conversation.
    pub conversation: String,
    /// The giving agent.
    pub from: String,
    /// The receiving agent.
    pub to: String,
    /// Who did it (the giving agent, or whom the speaker policy allows).
    pub by: String,
    /// Why.
    pub reason: String,
    /// What history the receiver is given.
    pub history: TransferHistory,
    /// For `filtered`: which part (`{"last": N}`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub filter: Option<Value>,
    /// Its progress.
    pub state: TransferState,
    /// RFC3339.
    pub at: String,
    /// Why it was refused.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// A handoff's progress.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum HandoffState {
    /// Announced on the target, not yet picked up.
    Pending,
    /// The session is bound there.
    Done,
    /// It could not move.
    Failed,
}

/// A session moving to another conversation (Hermes `/handoff`, kept by the naming rule).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Handoff {
    /// The session (`<harness>:<id>`).
    pub session: String,
    /// The target conversation.
    pub to: String,
    /// Who asked.
    pub by: String,
    /// Its progress.
    pub state: HandoffState,
    /// Why it failed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    /// RFC3339.
    pub at: String,
}

/// A cost, as the provider reported it or a rate table priced it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Cost {
    /// The amount.
    pub amount: f64,
    /// ISO currency.
    pub currency: String,
    /// `provider` or `rate_table`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,
}

impl Eq for Cost {}

/// One measured use of a model, from the harness's own records (G3).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Usage {
    /// RFC3339.
    pub at: String,
    /// The model.
    pub model: String,
    /// Input tokens.
    pub input_tokens: u64,
    /// Output tokens.
    pub output_tokens: u64,
    /// Cache reads, when measured.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cache_read_tokens: Option<u64>,
    /// Cache writes, when measured.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cache_write_tokens: Option<u64>,
    /// Its cost, when known.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cost: Option<Cost>,
}

/// A session's usage, kept on the session and summed upward (session โ†’ agent โ†’ sponsor โ†’ Org).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SessionUsage {
    /// The agent the session belongs to, when it has one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub agent: Option<String>,
    /// The records.
    #[serde(default)]
    pub usage: Vec<Usage>,
}

/// A budget's window.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum BudgetPeriod {
    /// One run.
    Run,
    /// A calendar day.
    Day,
    /// A calendar month.
    Month,
}

/// What a budget bounds.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct BudgetLimit {
    /// Total tokens.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tokens: Option<u64>,
    /// Total cost.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cost: Option<Cost>,
}

/// A per-action gate on a profile's agents: a start or resume over it is refused, with the reason,
/// and the refusal is told to the manager and the sponsor. It never pauses or ends anything itself.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Budget {
    /// The window.
    pub period: BudgetPeriod,
    /// The bound.
    pub limit: BudgetLimit,
}