supercode_interchange/ontology/surface.rs
1//! The conversation-on-a-surface nouns (ORCH-3 / ORCH-6): where a
2//! conversation is reached, why it exists, which job it belongs to, where it
3//! moved. They are the vocabulary a [`crate::ontology::Binding`] is made of and
4//! the wire block every discovery row carries.
5
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9/// ORCH-3: why a session exists (the trigger noun; `docs/HERMES-IDEAL-SUPPORT-DESIGN.md` §3).
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
11#[serde(rename_all = "snake_case")]
12pub enum Trigger {
13 /// A person started it on a terminal, IDE, or chat surface.
14 Human,
15 /// A message on a channel opened it.
16 Channel,
17 /// A scheduled job fired it.
18 Cron,
19 /// A periodic main-session turn.
20 Heartbeat,
21 /// An inbound webhook / mapped hook.
22 Webhook,
23 /// Spawned by a parent session (delegate / subagent).
24 Parent,
25 /// An HTTP API / bridge client.
26 Api,
27 /// The source does not say.
28 #[default]
29 Unknown,
30}
31
32/// ORCH-3: the conversation identity on a surface. Full tuple on a channel;
33/// degenerate (all `None`) on a terminal.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
35pub struct SurfaceKey {
36 /// The harness's own key string, verbatim.
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub key: Option<String>,
39 /// Transport / platform (`telegram`, `slack`, `acp`, …).
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub platform: Option<String>,
42 /// `dm` | `group` | `channel` | `thread` | `main` (harness vocabulary, verbatim).
43 /// `chat_type` is the orchestrator IR's spelling of the same field.
44 #[serde(default, skip_serializing_if = "Option::is_none", alias = "chat_type")]
45 pub kind: Option<String>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 /// The chat / group / channel id on the platform.
48 pub chat_id: Option<String>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 /// The thread or topic within the chat.
51 pub thread_id: Option<String>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 /// The participant, when the surface is per person.
54 pub participant_id: Option<String>,
55}
56
57impl SurfaceKey {
58 /// A key with no platform is not a channel surface.
59 pub fn is_channel(&self) -> bool {
60 self.platform.is_some()
61 }
62}
63
64/// ORCH-3: the job a recurring session belongs to.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
66pub struct Recurrence {
67 /// The job id.
68 pub job_id: String,
69 /// `cron` | `heartbeat`.
70 #[serde(default = "recurrence_kind_cron")]
71 pub kind: String,
72}
73
74/// ORCH-3: a conversation moved to another surface (Hermes `handoff_*`).
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
76pub struct CrossSurface {
77 /// `pending` | `done` | `failed` (the source's own word).
78 pub state: String,
79 /// The destination platform.
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub platform: Option<String>,
82 /// The failure, when `state` is `failed`.
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub error: Option<String>,
85}
86
87/// ORCH-3 / UNI-9: the typed workspace, derived — never stored.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
89#[serde(rename_all = "snake_case")]
90pub enum WorkspaceKind {
91 /// A repository checkout (a cwd).
92 Repo,
93 /// A chat surface with no checkout.
94 Channel,
95 /// Neither.
96 None,
97}
98
99/// ORCH-6: the wire form of [`SessionMeta::workspace`] — a typed workspace
100/// carried on a discovered or loaded row. The D2 precedence that produces it
101/// lives in `workspace()` (ORCH-3's contract); this only names the result.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
103pub struct WorkspaceRef {
104 /// `repo` | `channel` | `none`.
105 pub kind: WorkspaceKind,
106 /// Path for `repo`, `<platform>:<chat_id>` for `channel`, `None` for `none`.
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub value: Option<String>,
109}
110
111fn recurrence_kind_cron() -> String {
112 "cron".to_string()
113}