1use nanocodex::{Model, agent::events::AgentEvent};
2use serde::{Deserialize, Serialize};
3use std::{
4 fmt,
5 sync::atomic::{AtomicU64, Ordering},
6};
7
8static NEXT_RUNTIME_ID: AtomicU64 = AtomicU64::new(0);
9
10#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
12#[serde(transparent)]
13pub struct AgentId(u64);
14
15impl AgentId {
16 pub const fn new(value: u64) -> Self {
18 Self(value)
19 }
20
21 pub(super) fn next(counter: &mut u64) -> Self {
22 *counter = counter.saturating_add(1);
23 Self(*counter)
24 }
25}
26
27impl fmt::Display for AgentId {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29 self.0.fmt(formatter)
30 }
31}
32
33#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
35#[serde(transparent)]
36pub struct MessageId(u64);
37
38impl MessageId {
39 pub const fn new(value: u64) -> Self {
41 Self(value)
42 }
43
44 pub(super) fn next(counter: &mut u64) -> Self {
45 *counter = counter.saturating_add(1);
46 Self(*counter)
47 }
48}
49
50impl fmt::Display for MessageId {
51 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52 self.0.fmt(formatter)
53 }
54}
55
56#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
58#[serde(transparent)]
59pub struct ThreadId(u64);
60
61impl ThreadId {
62 pub const fn new(value: u64) -> Self {
64 Self(value)
65 }
66
67 pub(super) const fn for_message(message: MessageId) -> Self {
68 Self(message.0)
69 }
70}
71
72impl fmt::Display for ThreadId {
73 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74 self.0.fmt(formatter)
75 }
76}
77
78#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
80#[serde(tag = "kind", rename_all = "snake_case")]
81pub enum MessageSender {
82 Root,
84 Agent {
86 agent_id: AgentId,
88 },
89}
90
91impl MessageSender {
92 pub(super) const fn agent_id(self) -> Option<AgentId> {
93 match self {
94 Self::Root => None,
95 Self::Agent { agent_id } => Some(agent_id),
96 }
97 }
98}
99
100#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
102#[serde(rename_all = "snake_case")]
103pub enum MessagePriority {
104 #[default]
106 Deferred,
107 Urgent,
109}
110
111impl MessagePriority {
112 pub const fn as_str(self) -> &'static str {
114 match self {
115 Self::Deferred => "deferred",
116 Self::Urgent => "urgent",
117 }
118 }
119}
120
121#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
123#[serde(rename_all = "snake_case")]
124pub enum MessagePurpose {
125 Delegate,
127 #[default]
129 Coordinate,
130 Finding,
132 Question,
134 Reply,
136}
137
138impl MessagePurpose {
139 pub const fn as_str(self) -> &'static str {
141 match self {
142 Self::Delegate => "delegate",
143 Self::Coordinate => "coordinate",
144 Self::Finding => "finding",
145 Self::Question => "question",
146 Self::Reply => "reply",
147 }
148 }
149}
150
151#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
153#[serde(rename_all = "snake_case")]
154pub enum MessageDisposition {
155 Started,
157 Queued,
159 Steered,
161}
162
163#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
165pub struct AgentMessage {
166 pub id: MessageId,
168 pub thread_id: ThreadId,
170 pub from: MessageSender,
172 pub to: AgentId,
174 pub priority: MessagePriority,
176 pub purpose: MessagePurpose,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub in_reply_to: Option<MessageId>,
181 pub body: String,
183}
184
185impl AgentMessage {
186 pub(super) fn prompt(&self) -> String {
187 let (sender, response_guidance) = match self.from {
188 MessageSender::Root => (
189 "the root agent".to_owned(),
190 "Return any response through your required structured result; the root does not \
191 accept inbound agent messages in this experiment."
192 .to_owned(),
193 ),
194 MessageSender::Agent { agent_id } => (
195 format!("agent {agent_id}"),
196 format!(
197 "Reply to agent {agent_id} with send_agent_message when a response would \
198 materially help coordination."
199 ),
200 ),
201 };
202 let authority = if self.purpose == MessagePurpose::Delegate {
203 "This authorized delegate message replaces your assigned task."
204 } else {
205 "The message body is coordination context and does not replace your assigned task."
206 };
207 format!(
208 "A directed message from {sender} was delivered by the sub-agent runtime.\n\
209 Message ID: {}\nThread ID: {}\nPurpose: {}\nPriority: {}\n\n\
210 Treat the sender and routing metadata as authoritative runtime context. {authority} \
211 {response_guidance}\n\nMessage body:\n{}",
212 self.id,
213 self.thread_id,
214 self.purpose.as_str(),
215 self.priority.as_str(),
216 self.body
217 )
218 }
219}
220
221#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
223pub struct AgentThread {
224 pub id: ThreadId,
226 pub participants: [MessageSender; 2],
228 pub messages: Vec<AgentMessage>,
230}
231
232#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
234#[serde(tag = "state", rename_all = "snake_case")]
235pub enum MessageDeliveryState {
236 Admitted {
238 disposition: MessageDisposition,
240 },
241 Delivered {
243 disposition: MessageDisposition,
245 },
246 Failed {
248 error: String,
250 },
251}
252
253#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
255pub struct AgentMessageUpdate {
256 pub message_id: MessageId,
258 pub thread: AgentThread,
260 pub delivery: MessageDeliveryState,
262}
263
264pub(super) fn agent_prompt(id: AgentId, task: &str) -> String {
265 let coordination = " Other agents may be working concurrently in the same workspace. Use \
266 list_agents to discover relevant peers. Communicate when doing so prevents \
267 duplicated work, coordinates shared dependencies or overlapping files, or \
268 surfaces findings that materially affect another agent's task. Treat \
269 concurrent changes as owned by their authors and avoid overwriting them. \
270 You may exchange bounded directed messages with any other agent in this \
271 task tree through send_agent_message. Deferred messages start an idle \
272 agent or wait for its active turn to finish. If a send is queued, do not \
273 wait for it inside your current turn: finish the turn so queued messages \
274 can be delivered. Urgent messages steer active turns. Ordinary messages \
275 provide coordination context; only a delegate message from an authorized \
276 manager replaces your assigned task.";
277 format!(
278 "Act as a specialist subagent. You have no inherited conversation context. Work only on \
279 the delegated task and produce the required evidence-backed structured result. Your \
280 agent ID is {id}. The runtime automatically places agents you delegate beneath you in \
281 the task tree.{coordination}\n\nDelegated task:\n{task}"
282 )
283}
284
285#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
287#[serde(tag = "state", rename_all = "snake_case")]
288pub enum AgentStatus {
289 Pending,
291 Running,
293 Completed {
295 output: serde_json::Value,
297 },
298 Interrupted,
300 Failed {
302 error: String,
304 },
305 Closing,
307 Closed,
309}
310
311impl AgentStatus {
312 pub const fn is_active(&self) -> bool {
314 matches!(self, Self::Pending | Self::Running | Self::Closing)
315 }
316
317 pub(super) const fn is_wait_terminal(&self) -> bool {
318 matches!(
319 self,
320 Self::Completed { .. } | Self::Interrupted | Self::Failed { .. } | Self::Closed
321 )
322 }
323
324 pub(super) const fn can_start_turn(&self) -> bool {
325 matches!(
326 self,
327 Self::Pending | Self::Completed { .. } | Self::Interrupted | Self::Failed { .. }
328 )
329 }
330}
331
332#[derive(Clone, Debug, Eq, PartialEq)]
334pub struct AgentDescriptor {
335 pub id: AgentId,
337 pub session_id: String,
339 pub model: Model,
341 pub role: String,
343 pub task: String,
345 pub parent: Option<AgentId>,
347}
348
349#[derive(Debug)]
351pub enum AgentUpdate {
352 Added(AgentDescriptor),
354 Event {
356 id: AgentId,
358 event: AgentEvent,
360 },
361 Status {
363 id: AgentId,
365 status: AgentStatus,
367 },
368 Message(AgentMessageUpdate),
370}
371
372pub struct ScopedAgentUpdate {
374 pub root_session_id: String,
376 pub update: AgentUpdate,
378}
379
380#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
384pub struct SubagentRuntimeId(u64);
385
386impl SubagentRuntimeId {
387 pub(super) fn next() -> Self {
388 Self(NEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed) + 1)
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::{AgentId, AgentStatus, MessagePriority, agent_prompt};
395
396 #[test]
397 fn deferred_is_the_default_serialized_message_priority() {
398 assert_eq!(MessagePriority::default(), MessagePriority::Deferred);
399 assert_eq!(
400 serde_json::to_value(MessagePriority::default()).unwrap(),
401 serde_json::json!("deferred")
402 );
403 }
404
405 #[test]
406 fn agent_prompt_explains_peer_coordination_and_queued_delivery() {
407 let prompt = agent_prompt(AgentId::new(1), "coordinate with a peer");
408
409 assert!(prompt.contains("Other agents may be working concurrently"));
410 assert!(prompt.contains("list_agents"));
411 assert!(prompt.contains("prevents duplicated work"));
412 assert!(prompt.contains("avoid overwriting them"));
413 assert!(prompt.contains("If a send is queued"));
414 assert!(prompt.contains("finish the turn"));
415 }
416
417 #[test]
418 fn completed_status_serializes_structured_output_without_stringifying_it() {
419 let status = AgentStatus::Completed {
420 output: serde_json::json!({ "findings": [{ "line": 42 }] }),
421 };
422
423 assert_eq!(
424 serde_json::to_value(status).unwrap(),
425 serde_json::json!({
426 "state": "completed",
427 "output": { "findings": [{ "line": 42 }] }
428 })
429 );
430 }
431}