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 /// A board's dispatcher spawned it to work a task (the workflow layer).
28 Task,
29 /// The source does not say.
30 #[default]
31 Unknown,
32}
33
34/// ORCH-3: the conversation identity on a surface. Full tuple on a channel;
35/// degenerate (all `None`) on a terminal.
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
37pub struct SurfaceKey {
38 /// The harness's own key string, verbatim.
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub key: Option<String>,
41 /// Transport / platform (`telegram`, `slack`, `acp`, …).
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub platform: Option<String>,
44 /// `dm` | `group` | `channel` | `thread` | `main` (harness vocabulary, verbatim).
45 /// `chat_type` is the orchestrator IR's spelling of the same field.
46 #[serde(default, skip_serializing_if = "Option::is_none", alias = "chat_type")]
47 pub kind: Option<String>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 /// The chat / group / channel id on the platform.
50 pub chat_id: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 /// The thread or topic within the chat.
53 pub thread_id: Option<String>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 /// The participant, when the surface is per person.
56 pub participant_id: Option<String>,
57}
58
59impl SurfaceKey {
60 /// A key with no platform is not a channel surface.
61 pub fn is_channel(&self) -> bool {
62 self.platform.is_some()
63 }
64}
65
66/// ORCH-3: the job a recurring session belongs to.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
68pub struct Recurrence {
69 /// The job id.
70 pub job_id: String,
71 /// `cron` | `heartbeat`.
72 #[serde(default = "recurrence_kind_cron")]
73 pub kind: String,
74}
75
76/// ORCH-3: a conversation moved to another surface (Hermes `handoff_*`).
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78pub struct CrossSurface {
79 /// `pending` | `done` | `failed` (the source's own word).
80 pub state: String,
81 /// The destination platform.
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub platform: Option<String>,
84 /// The failure, when `state` is `failed`.
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub error: Option<String>,
87}
88
89/// ORCH-3 / UNI-9: the typed workspace, derived — never stored.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
91#[serde(rename_all = "snake_case")]
92pub enum WorkspaceKind {
93 /// A repository checkout (a cwd).
94 Repo,
95 /// A chat surface with no checkout.
96 Channel,
97 /// Neither.
98 None,
99}
100
101/// ORCH-6: the wire form of [`SessionMeta::workspace`] — a typed workspace
102/// carried on a discovered or loaded row. The D2 precedence that produces it
103/// lives in `workspace()` (ORCH-3's contract); this only names the result.
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
105pub struct WorkspaceRef {
106 /// `repo` | `channel` | `none`.
107 pub kind: WorkspaceKind,
108 /// Path for `repo`, `<platform>:<chat_id>` for `channel`, `None` for `none`.
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub value: Option<String>,
111}
112
113fn recurrence_kind_cron() -> String {
114 "cron".to_string()
115}