1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
//! Durable agent metadata. Debug deliberately excludes all free text.
use serde::{Deserialize, Serialize};
/// A harness label, never a selector of server behaviour.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgentHarness {
/// Codex with channel wake.
#[serde(rename = "cdx")]
Cdx,
/// Claude Code.
#[serde(rename = "claude-code")]
ClaudeCode,
/// Codex through the ACP host.
#[serde(rename = "codex")]
Codex,
/// Gemini CLI through the ACP host.
#[serde(rename = "gemini-cli")]
GeminiCli,
/// `OpenCode` through the ACP host.
#[serde(rename = "opencode")]
Opencode,
/// Pi through its separately launched host.
#[serde(rename = "pi")]
Pi,
}
/// Fields a person approves when creating an agent.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgentSpec {
/// Its declared harness.
pub harness: AgentHarness,
/// One sentence stating its job.
pub purpose: String,
/// The service it builds, when it is a builder.
pub service: Option<String>,
}
/// Events retained forever in the exchange's store.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
tag = "event",
content = "detail",
rename_all = "snake_case",
deny_unknown_fields
)]
pub enum AgentEvent {
/// A person approved a new station.
Create(AgentSpec),
/// Lys answered the deliberate start.
Start,
/// Lys answered the deliberate stop.
Stop,
/// A verified ticket subscribed its inbox.
Present {
/// The connection's process id.
connection: u64,
},
/// That connection closed with this stable transport or disconnect name.
Gone {
/// The closed connection.
connection: u64,
/// The stable reason, never a transport's payload.
reason: String,
},
/// The host negotiated this session; old histories have no such event.
Session {
session: String,
protocol_version: u8,
load_session: bool,
},
/// An observed host ending never invents resume capability.
SessionClosed {
exit_status: i32,
session_state: String,
},
/// The station is permanently retired; its history remains.
Retire,
}
/// A transition always names its actor and time.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgentTransition {
/// The actor proved by the pass, or the server for observed departures.
pub holder: String,
/// Epoch milliseconds of this transition.
pub at_ms: u64,
/// What changed.
pub change: AgentEvent,
}
/// One station, as returned after restoration.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgentRecord {
/// One segment, exactly the Lys agent holder id.
pub name: String,
/// The profile's harness label.
pub harness: AgentHarness,
/// One sentence stating its job.
pub purpose: String,
/// The person who created it.
pub owner: String,
/// The service it builds.
pub service: Option<String>,
/// Epoch milliseconds of creation.
pub created_at_ms: u64,
/// Every transition, oldest first; retirement never removes one.
pub transitions: Vec<AgentTransition>,
}
impl AgentRecord {
/// Whether this station still occupies a configured slot.
#[must_use]
pub fn active(&self) -> bool {
self.transitions
.last()
.is_none_or(|transition| !matches!(transition.change, AgentEvent::Retire))
}
/// Its last observed connection; lifecycle requests do not invent presence.
#[must_use]
pub fn connection(&self) -> Option<u64> {
self.seat().map(|(connection, _)| connection)
}
/// The present connection and the epoch milliseconds it was admitted.
#[must_use]
pub fn seat(&self) -> Option<(u64, u64)> {
self.transitions
.iter()
.rev()
.find_map(|transition| match transition.change {
AgentEvent::Present { connection } => Some(Some((connection, transition.at_ms))),
AgentEvent::Gone { .. } | AgentEvent::Retire => Some(None),
_ => None,
})
.flatten()
}
}
impl std::fmt::Debug for AgentRecord {
fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
out.debug_struct("AgentRecord")
.field("harness", &self.harness)
.field("transitions", &self.transitions.len())
.finish_non_exhaustive()
}
}
/// Immutable prepared words, supplied only by the approved-draft executor.
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgentAct {
/// Name of the placed agent.
pub name: String,
/// The original verified holder, kept in the immutable draft.
pub holder: String,
/// The caller's responsible person, proved by Lys; never a request override.
pub owner: String,
/// Preparation time, in epoch milliseconds.
pub at_ms: u64,
/// The deliberate event.
pub change: AgentEvent,
}
impl AgentAct {
/// Prepares immutable words from verified claims. An agent's builders
/// count against its responsible person, so changing creator agent ids
/// cannot evade the person's configured bound.
///
/// # Errors
/// The verified holder is neither a person nor an agent with a responsible person.
pub fn prepare(
name: String,
change: AgentEvent,
pass: &lys_pass::VerifiedPass,
at_ms: u64,
) -> Result<Self, super::error::AgentError> {
let holder = &pass.claims().holder;
let owner = match holder.kind.as_str() {
"person" => holder.id.as_str(),
"agent" => holder
.responsible
.as_deref()
.ok_or(super::error::AgentError::Invalid(
"agent responsible person",
))?,
_ => {
return Err(super::error::AgentError::Invalid(
"agent creation holder kind",
));
}
};
if owner.is_empty() {
return Err(super::error::AgentError::Invalid(
"agent responsible person",
));
}
Ok(Self {
name,
holder: holder.id.clone(),
owner: owner.into(),
at_ms,
change,
})
}
}