Skip to main content

supercode_interchange/orchestration/
agent.rs

1//! The agent layer (mirrored ontology C8, `docs/architecture/orchestrator.md` ยง2.10): declared
2//! agents, conversations separate from sessions (G5, as amended by M3), transfers and handoffs
3//! (G2), usage and budgets (G3). Bindings name their conversation and agent (G1); routes name an
4//! agent. Agent identity itself (ids, sponsor, lineage) lives with Teams and the orchestrator's
5//! agent store; these records are the home's intent and its conversations' record.
6
7use std::collections::BTreeMap;
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::ontology::SurfaceKey;
14
15/// One agent this home declares: who answers, and which profile is its template.
16/// A named profile implies its own agent of the same name; a declaration names others (several
17/// agents may share one profile) and the parent each is declared under.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
19pub struct AgentDecl {
20    /// Unique among the home's agents; a route, a speaker policy and a binding name it.
21    pub name: String,
22    /// The profile it runs as (its layered template).
23    pub profile: String,
24    /// A display label, not identity.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub label: Option<String>,
27    /// The agent it is declared under, if not the home's root.
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub parent: Option<String>,
30    /// A person set on purpose as its sponsor; absent, the sponsor is inherited.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub sponsor: Option<String>,
33    /// What a source format said about it that the IR has no field for, verbatim.
34    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
35    pub residue: BTreeMap<String, Value>,
36}
37
38/// `thread`: a root and its replies, wherever it is shown; `private`: a harness session's own input.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
40#[serde(rename_all = "snake_case")]
41pub enum ConversationKind {
42    /// A root and its replies (a Room conversation, a chat thread, a mailbox exchange).
43    Thread,
44    /// A harness session's own conversation with its owner; its history is that transcript.
45    Private,
46}
47
48/// How a participant is addressed on a thread (amendment M3).
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
50#[serde(rename_all = "snake_case")]
51pub enum ParticipantRole {
52    /// Addressed: expected to act.
53    To,
54    /// Copied: receives every reply.
55    Cc,
56}
57
58/// A person or an agent in a conversation.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
60pub struct Participant {
61    /// An agent's name, or a person as `<platform>:<user id>` (an identity observation).
62    pub principal: String,
63    /// To or CC.
64    pub role: ParticipantRole,
65    /// RFC3339.
66    pub since: String,
67    /// RFC3339, when it left.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub until: Option<String>,
70}
71
72/// Which bound agents receive a line on a shared surface (G1).
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
74#[serde(tag = "kind", rename_all = "snake_case")]
75pub enum SpeakerPolicy {
76    /// Mentions pick the agent; `default` takes the rest (a Hermes or OpenClaw surface: one agent).
77    Addressed {
78        /// The agent that takes an unaddressed line.
79        #[serde(default, skip_serializing_if = "Option::is_none")]
80        default: Option<String>,
81    },
82    /// Every bound agent gets every line.
83    All,
84    /// Agents take lines in this order.
85    RoundRobin {
86        /// The order.
87        order: Vec<String>,
88    },
89    /// An agent picks the next speaker (an event out and back, never a hidden model call).
90    Selector {
91        /// The selecting agent.
92        selector: String,
93    },
94    /// The meeting's adapter decides (RH2's floor, the voice pack).
95    Floor,
96}
97
98impl Default for SpeakerPolicy {
99    fn default() -> Self {
100        Self::Addressed { default: None }
101    }
102}
103
104/// Where a conversation's record lives.
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
106#[serde(tag = "kind", rename_all = "snake_case")]
107pub enum HistoryRef {
108    /// The platform's own thread (a chat thread, a Room conversation).
109    Platform,
110    /// The mailbox thread.
111    Mailbox {
112        /// The thread id.
113        thread: String,
114    },
115    /// A private conversation's history: its owning session's transcript.
116    Transcript {
117        /// The session (`<harness>:<id>`).
118        session: String,
119    },
120}
121
122impl Default for HistoryRef {
123    fn default() -> Self {
124        Self::Platform
125    }
126}
127
128/// A conversation, a Resource separate from the sessions bound to it (G5, amended by M3).
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
130pub struct Conversation {
131    /// Its id.
132    pub id: String,
133    /// Thread or private.
134    pub kind: ConversationKind,
135    /// Where a thread is shown; no profile is part of it.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub surface: Option<SurfaceKey>,
138    /// People and agents in it.
139    #[serde(default)]
140    pub participants: Vec<Participant>,
141    /// Who receives a line.
142    #[serde(default)]
143    pub speaker: SpeakerPolicy,
144    /// Where its record lives.
145    #[serde(default)]
146    pub history: HistoryRef,
147    /// The agent a completed transfer gave the conversation to; an unaddressed line goes to it.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub holder: Option<String>,
150    /// The agent that took the last line (round robin's position).
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub last_speaker: Option<String>,
153}
154
155/// How much of the conversation a transfer's receiver is given.
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
157#[serde(rename_all = "snake_case")]
158pub enum TransferHistory {
159    /// All of it (the default).
160    Full,
161    /// What the filter keeps.
162    Filtered,
163    /// None.
164    None,
165}
166
167/// A transfer's progress.
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
169#[serde(rename_all = "snake_case")]
170pub enum TransferState {
171    /// Asked, not yet applied.
172    Pending,
173    /// The receiver holds the conversation.
174    Done,
175    /// The receiver may not take part.
176    Refused,
177}
178
179/// One agent handing a conversation to another (OpenAI handoff, AutoGen Swarm, voice `hand_off`).
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
181pub struct Transfer {
182    /// The conversation.
183    pub conversation: String,
184    /// The giving agent.
185    pub from: String,
186    /// The receiving agent.
187    pub to: String,
188    /// Who did it (the giving agent, or whom the speaker policy allows).
189    pub by: String,
190    /// Why.
191    pub reason: String,
192    /// What history the receiver is given.
193    pub history: TransferHistory,
194    /// For `filtered`: which part (`{"last": N}`).
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub filter: Option<Value>,
197    /// Its progress.
198    pub state: TransferState,
199    /// RFC3339.
200    pub at: String,
201    /// Why it was refused.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub error: Option<String>,
204}
205
206/// A handoff's progress.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
208#[serde(rename_all = "snake_case")]
209pub enum HandoffState {
210    /// Announced on the target, not yet picked up.
211    Pending,
212    /// The session is bound there.
213    Done,
214    /// It could not move.
215    Failed,
216}
217
218/// A session moving to another conversation (Hermes `/handoff`, kept by the naming rule).
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
220pub struct Handoff {
221    /// The session (`<harness>:<id>`).
222    pub session: String,
223    /// The target conversation.
224    pub to: String,
225    /// Who asked.
226    pub by: String,
227    /// Its progress.
228    pub state: HandoffState,
229    /// Why it failed.
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub error: Option<String>,
232    /// RFC3339.
233    pub at: String,
234}
235
236/// A cost, as the provider reported it or a rate table priced it.
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
238pub struct Cost {
239    /// The amount.
240    pub amount: f64,
241    /// ISO currency.
242    pub currency: String,
243    /// `provider` or `rate_table`.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub source: Option<String>,
246}
247
248impl Eq for Cost {}
249
250/// One measured use of a model, from the harness's own records (G3).
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
252pub struct Usage {
253    /// RFC3339.
254    pub at: String,
255    /// The model.
256    pub model: String,
257    /// Input tokens.
258    pub input_tokens: u64,
259    /// Output tokens.
260    pub output_tokens: u64,
261    /// Cache reads, when measured.
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub cache_read_tokens: Option<u64>,
264    /// Cache writes, when measured.
265    #[serde(default, skip_serializing_if = "Option::is_none")]
266    pub cache_write_tokens: Option<u64>,
267    /// Its cost, when known.
268    #[serde(default, skip_serializing_if = "Option::is_none")]
269    pub cost: Option<Cost>,
270}
271
272/// A session's usage, kept on the session and summed upward (session โ†’ agent โ†’ sponsor โ†’ Org).
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
274pub struct SessionUsage {
275    /// The agent the session belongs to, when it has one.
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub agent: Option<String>,
278    /// The records.
279    #[serde(default)]
280    pub usage: Vec<Usage>,
281}
282
283/// A budget's window.
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
285#[serde(rename_all = "snake_case")]
286pub enum BudgetPeriod {
287    /// One run.
288    Run,
289    /// A calendar day.
290    Day,
291    /// A calendar month.
292    Month,
293}
294
295/// What a budget bounds.
296#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
297pub struct BudgetLimit {
298    /// Total tokens.
299    #[serde(default, skip_serializing_if = "Option::is_none")]
300    pub tokens: Option<u64>,
301    /// Total cost.
302    #[serde(default, skip_serializing_if = "Option::is_none")]
303    pub cost: Option<Cost>,
304}
305
306/// A per-action gate on a profile's agents: a start or resume over it is refused, with the reason,
307/// and the refusal is told to the manager and the sponsor. It never pauses or ends anything itself.
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
309pub struct Budget {
310    /// The window.
311    pub period: BudgetPeriod,
312    /// The bound.
313    pub limit: BudgetLimit,
314}