1use crate::event_protocol::{run_event_envelope_v1, EventEnvelopeV1, EVENT_ENVELOPE_V1_VERSION};
9pub use crate::release::AGENT_PROTOCOL_V1;
10use crate::run::{RunEventPage, RunEventRecord, RunStatus};
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13use thiserror::Error;
14
15pub const AGENT_PROTOCOL_MAX_ID_BYTES: usize = 256;
16pub const AGENT_PROTOCOL_MAX_REASON_BYTES: usize = 1_024;
17pub const AGENT_PROTOCOL_MAX_PROMPT_BYTES: usize = 64 * 1024;
18pub const AGENT_PROTOCOL_MAX_EVENT_TYPE_BYTES: usize = 128;
19pub const AGENT_PROTOCOL_MAX_EVENT_PAYLOAD_BYTES: usize = 64 * 1024;
20pub const AGENT_PROTOCOL_MAX_EVENT_METADATA_BYTES: usize = 16 * 1024;
21pub const AGENT_PROTOCOL_MAX_EVENT_RECORD_BYTES: usize = 64 * 1024;
22pub const AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE: usize = 64;
23pub const AGENT_PROTOCOL_MAX_EVENT_PAGE_BYTES: usize = 6 * 1024 * 1024;
24
25pub const AGENT_PROTOCOL_COMMAND_HTTP_PATH_V1: &str = "/v1/agent/commands";
27
28pub const AGENT_PROTOCOL_EVENT_PAGE_HTTP_PATH_V1: &str = "/v1/agent/events:page";
30
31#[derive(Debug, Clone, PartialEq, Eq, Error)]
33pub enum AgentProtocolError {
34 #[error("unsupported A3S Code Agent protocol schema")]
35 UnsupportedSchema,
36 #[error("invalid A3S Code Agent protocol field: {0}")]
37 InvalidField(&'static str),
38 #[error("A3S Code Agent protocol identity or sequence does not match")]
39 IdentityMismatch,
40 #[error("A3S Code Agent protocol value exceeds its bounded encoding")]
41 Encoding,
42}
43
44impl AgentProtocolError {
45 pub const fn code(&self) -> &'static str {
47 match self {
48 Self::UnsupportedSchema => "a3s.code.agent_protocol.unsupported_schema",
49 Self::InvalidField(_) => "a3s.code.agent_protocol.invalid_field",
50 Self::IdentityMismatch => "a3s.code.agent_protocol.identity_mismatch",
51 Self::Encoding => "a3s.code.agent_protocol.encoding",
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct AgentProtocolRunIdentityV1 {
60 pub schema: String,
61 pub protocol: String,
62 pub agent_release_identity: String,
63 pub session_id: String,
64 pub run_id: String,
65}
66
67impl AgentProtocolRunIdentityV1 {
68 pub const SCHEMA: &'static str = "a3s.code.agent-run-identity.v1";
69
70 pub fn validate(&self) -> Result<(), AgentProtocolError> {
71 validate_schema(&self.schema, Self::SCHEMA)?;
72 if self.protocol != AGENT_PROTOCOL_V1 {
73 return Err(AgentProtocolError::InvalidField("protocol"));
74 }
75 validate_lower_sha256("agent_release_identity", &self.agent_release_identity)?;
76 validate_id("session_id", &self.session_id)?;
77 validate_id("run_id", &self.run_id)
78 }
79
80 pub fn digest(&self) -> Result<String, AgentProtocolError> {
81 digest_validated(self, || self.validate())
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct AgentProtocolRunStartV1 {
89 pub schema: String,
90 pub request_id: String,
91 pub identity: AgentProtocolRunIdentityV1,
92 pub prompt: String,
93}
94
95impl AgentProtocolRunStartV1 {
96 pub const SCHEMA: &'static str = "a3s.code.agent-run-start.v1";
97
98 pub fn validate(&self) -> Result<(), AgentProtocolError> {
99 validate_schema(&self.schema, Self::SCHEMA)?;
100 validate_id("request_id", &self.request_id)?;
101 self.identity.validate()?;
102 if self.prompt.trim().is_empty()
103 || self.prompt.len() > AGENT_PROTOCOL_MAX_PROMPT_BYTES
104 || self.prompt.contains('\0')
105 {
106 return Err(AgentProtocolError::InvalidField("prompt"));
107 }
108 Ok(())
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(deny_unknown_fields)]
115pub struct AgentProtocolRunCancelV1 {
116 pub schema: String,
117 pub request_id: String,
118 pub identity: AgentProtocolRunIdentityV1,
119 pub reason: String,
120}
121
122impl AgentProtocolRunCancelV1 {
123 pub const SCHEMA: &'static str = "a3s.code.agent-run-cancel.v1";
124
125 pub fn validate(&self) -> Result<(), AgentProtocolError> {
126 validate_schema(&self.schema, Self::SCHEMA)?;
127 validate_id("request_id", &self.request_id)?;
128 self.identity.validate()?;
129 validate_single_line("reason", &self.reason, AGENT_PROTOCOL_MAX_REASON_BYTES)
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(deny_unknown_fields)]
136pub struct AgentProtocolRunRecoverV1 {
137 pub schema: String,
138 pub request_id: String,
139 pub identity: AgentProtocolRunIdentityV1,
140 pub checkpoint_run_id: String,
141}
142
143impl AgentProtocolRunRecoverV1 {
144 pub const SCHEMA: &'static str = "a3s.code.agent-run-recover.v1";
145
146 pub fn validate(&self) -> Result<(), AgentProtocolError> {
147 validate_schema(&self.schema, Self::SCHEMA)?;
148 validate_id("request_id", &self.request_id)?;
149 self.identity.validate()?;
150 validate_id("checkpoint_run_id", &self.checkpoint_run_id)?;
151 if self.checkpoint_run_id == self.identity.run_id {
152 return Err(AgentProtocolError::InvalidField("checkpoint_run_id"));
153 }
154 Ok(())
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum AgentProtocolCommandActionV1 {
162 Start,
163 Cancel,
164 Recover,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
170pub enum AgentProtocolCommandV1 {
171 Start { request: AgentProtocolRunStartV1 },
172 Cancel { request: AgentProtocolRunCancelV1 },
173 Recover { request: AgentProtocolRunRecoverV1 },
174}
175
176impl AgentProtocolCommandV1 {
177 pub const fn action(&self) -> AgentProtocolCommandActionV1 {
178 match self {
179 Self::Start { .. } => AgentProtocolCommandActionV1::Start,
180 Self::Cancel { .. } => AgentProtocolCommandActionV1::Cancel,
181 Self::Recover { .. } => AgentProtocolCommandActionV1::Recover,
182 }
183 }
184
185 pub fn request_id(&self) -> &str {
186 match self {
187 Self::Start { request } => &request.request_id,
188 Self::Cancel { request } => &request.request_id,
189 Self::Recover { request } => &request.request_id,
190 }
191 }
192
193 pub fn identity(&self) -> &AgentProtocolRunIdentityV1 {
194 match self {
195 Self::Start { request } => &request.identity,
196 Self::Cancel { request } => &request.identity,
197 Self::Recover { request } => &request.identity,
198 }
199 }
200
201 pub fn validate(&self) -> Result<(), AgentProtocolError> {
202 match self {
203 Self::Start { request } => request.validate(),
204 Self::Cancel { request } => request.validate(),
205 Self::Recover { request } => request.validate(),
206 }
207 }
208
209 pub fn digest(&self) -> Result<String, AgentProtocolError> {
210 digest_validated(self, || self.validate())
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "snake_case")]
217pub enum AgentProtocolRunStateV1 {
218 Created,
219 Planning,
220 Executing,
221 Verifying,
222 Completed,
223 Failed,
224 Cancelled,
225}
226
227impl AgentProtocolRunStateV1 {
228 pub const fn as_str(self) -> &'static str {
229 match self {
230 Self::Created => "created",
231 Self::Planning => "planning",
232 Self::Executing => "executing",
233 Self::Verifying => "verifying",
234 Self::Completed => "completed",
235 Self::Failed => "failed",
236 Self::Cancelled => "cancelled",
237 }
238 }
239
240 pub const fn is_terminal(self) -> bool {
241 matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
242 }
243}
244
245impl From<RunStatus> for AgentProtocolRunStateV1 {
246 fn from(value: RunStatus) -> Self {
247 match value {
248 RunStatus::Created => Self::Created,
249 RunStatus::Planning => Self::Planning,
250 RunStatus::Executing => Self::Executing,
251 RunStatus::Verifying => Self::Verifying,
252 RunStatus::Completed => Self::Completed,
253 RunStatus::Failed => Self::Failed,
254 RunStatus::Cancelled => Self::Cancelled,
255 }
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
261#[serde(deny_unknown_fields)]
262pub struct AgentProtocolCommandReceiptV1 {
263 pub schema: String,
264 pub action: AgentProtocolCommandActionV1,
265 pub request_id: String,
266 pub identity: AgentProtocolRunIdentityV1,
267 pub command_digest: String,
268 pub state: AgentProtocolRunStateV1,
269 pub latest_event_sequence_exclusive: u64,
270 pub observed_at_ms: u64,
271 pub replayed: bool,
272}
273
274impl AgentProtocolCommandReceiptV1 {
275 pub const SCHEMA: &'static str = "a3s.code.agent-command-receipt.v1";
276
277 pub fn validate(&self) -> Result<(), AgentProtocolError> {
278 validate_schema(&self.schema, Self::SCHEMA)?;
279 validate_id("request_id", &self.request_id)?;
280 self.identity.validate()?;
281 validate_lower_sha256("command_digest", &self.command_digest)?;
282 if self.observed_at_ms == 0 {
283 return Err(AgentProtocolError::InvalidField("observed_at_ms"));
284 }
285 Ok(())
286 }
287
288 pub fn validate_for(&self, command: &AgentProtocolCommandV1) -> Result<(), AgentProtocolError> {
289 command.validate()?;
290 self.validate()?;
291 if self.action != command.action()
292 || self.request_id != command.request_id()
293 || self.identity != *command.identity()
294 || self.command_digest != command.digest()?
295 {
296 return Err(AgentProtocolError::IdentityMismatch);
297 }
298 if self.action == AgentProtocolCommandActionV1::Cancel && !self.state.is_terminal() {
299 return Err(AgentProtocolError::InvalidField("state"));
300 }
301 Ok(())
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307#[serde(deny_unknown_fields)]
308pub struct AgentProtocolEventRecordV1 {
309 pub sequence: u64,
310 pub occurred_at_ms: u64,
311 pub event: EventEnvelopeV1,
312}
313
314impl AgentProtocolEventRecordV1 {
315 pub fn from_run_event(
316 record: &RunEventRecord,
317 identity: &AgentProtocolRunIdentityV1,
318 ) -> Result<Self, AgentProtocolError> {
319 identity.validate()?;
320 let sequence = u64::try_from(record.sequence)
321 .map_err(|_| AgentProtocolError::InvalidField("sequence"))?;
322 let event = run_event_envelope_v1(record, &identity.run_id, &identity.session_id)
323 .map_err(|_| AgentProtocolError::Encoding)?;
324 let projected = Self {
325 sequence,
326 occurred_at_ms: record.timestamp_ms,
327 event,
328 };
329 projected.validate_for(identity)?;
330 Ok(projected)
331 }
332
333 pub fn validate_for(
338 &self,
339 identity: &AgentProtocolRunIdentityV1,
340 ) -> Result<(), AgentProtocolError> {
341 if self.event.version != EVENT_ENVELOPE_V1_VERSION {
342 return Err(AgentProtocolError::InvalidField("event.version"));
343 }
344 validate_single_line(
345 "event.type",
346 &self.event.event_type,
347 AGENT_PROTOCOL_MAX_EVENT_TYPE_BYTES,
348 )?;
349 validate_json_size(
350 "event.payload",
351 &self.event.payload,
352 AGENT_PROTOCOL_MAX_EVENT_PAYLOAD_BYTES,
353 )?;
354 let metadata = self
355 .event
356 .metadata
357 .as_ref()
358 .ok_or(AgentProtocolError::InvalidField("event.metadata"))?;
359 validate_json_size(
360 "event.metadata",
361 metadata,
362 AGENT_PROTOCOL_MAX_EVENT_METADATA_BYTES,
363 )?;
364 let metadata = metadata
365 .as_object()
366 .ok_or(AgentProtocolError::InvalidField("event.metadata"))?;
367 let exact = metadata.get("session_id").and_then(|value| value.as_str())
368 == Some(identity.session_id.as_str())
369 && metadata.get("run_id").and_then(|value| value.as_str())
370 == Some(identity.run_id.as_str())
371 && metadata.get("sequence").and_then(|value| value.as_u64()) == Some(self.sequence)
372 && metadata
373 .get("timestamp_ms")
374 .and_then(|value| value.as_u64())
375 == Some(self.occurred_at_ms);
376 if !exact {
377 return Err(AgentProtocolError::IdentityMismatch);
378 }
379 let encoded = serde_json::to_vec(self).map_err(|_| AgentProtocolError::Encoding)?;
380 if encoded.len() > AGENT_PROTOCOL_MAX_EVENT_RECORD_BYTES {
381 return Err(AgentProtocolError::InvalidField("event"));
382 }
383 Ok(())
384 }
385}
386
387#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
389#[serde(deny_unknown_fields)]
390pub struct AgentProtocolEventPageRequestV1 {
391 pub schema: String,
392 pub identity: AgentProtocolRunIdentityV1,
393 pub after_event_sequence: Option<u64>,
394 pub limit: u16,
395}
396
397impl AgentProtocolEventPageRequestV1 {
398 pub const SCHEMA: &'static str = "a3s.code.agent-event-page-request.v1";
399
400 pub fn validate(&self) -> Result<(), AgentProtocolError> {
401 validate_schema(&self.schema, Self::SCHEMA)?;
402 self.identity.validate()?;
403 if self.limit == 0 || usize::from(self.limit) > AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE {
404 return Err(AgentProtocolError::InvalidField("limit"));
405 }
406 Ok(())
407 }
408
409 pub fn digest(&self) -> Result<String, AgentProtocolError> {
410 digest_validated(self, || self.validate())
411 }
412}
413
414#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
416#[serde(deny_unknown_fields)]
417pub struct AgentProtocolEventPageV1 {
418 pub schema: String,
419 pub identity: AgentProtocolRunIdentityV1,
420 pub after_event_sequence: Option<u64>,
421 pub first_available_sequence: Option<u64>,
422 pub latest_sequence_exclusive: u64,
423 pub next_after_event_sequence: Option<u64>,
424 pub state: AgentProtocolRunStateV1,
425 pub observed_at_ms: u64,
426 pub retention_gap: bool,
427 pub has_more: bool,
428 pub events: Vec<AgentProtocolEventRecordV1>,
429}
430
431impl AgentProtocolEventPageV1 {
432 pub const SCHEMA: &'static str = "a3s.code.agent-event-page.v1";
433
434 pub fn from_run_page(
435 identity: AgentProtocolRunIdentityV1,
436 state: RunStatus,
437 observed_at_ms: u64,
438 after_event_sequence: Option<usize>,
439 page: &RunEventPage,
440 ) -> Result<Self, AgentProtocolError> {
441 identity.validate()?;
442 let convert = |value: usize| {
443 u64::try_from(value).map_err(|_| AgentProtocolError::InvalidField("sequence"))
444 };
445 let events = page
446 .events
447 .iter()
448 .map(|record| AgentProtocolEventRecordV1::from_run_event(record, &identity))
449 .collect::<Result<Vec<_>, _>>()?;
450 let projected = Self {
451 schema: Self::SCHEMA.into(),
452 identity,
453 after_event_sequence: after_event_sequence.map(convert).transpose()?,
454 first_available_sequence: page.first_available_sequence.map(convert).transpose()?,
455 latest_sequence_exclusive: convert(page.latest_sequence_exclusive)?,
456 next_after_event_sequence: page.next_after_sequence.map(convert).transpose()?,
457 state: state.into(),
458 observed_at_ms,
459 retention_gap: page.retention_gap,
460 has_more: page.has_more,
461 events,
462 };
463 projected.validate()?;
464 Ok(projected)
465 }
466
467 pub fn validate(&self) -> Result<(), AgentProtocolError> {
468 validate_schema(&self.schema, Self::SCHEMA)?;
469 self.identity.validate()?;
470 if self.events.len() > AGENT_PROTOCOL_MAX_EVENTS_PER_PAGE {
471 return Err(AgentProtocolError::InvalidField("events"));
472 }
473 if self
474 .first_available_sequence
475 .is_some_and(|sequence| sequence >= self.latest_sequence_exclusive)
476 {
477 return Err(AgentProtocolError::InvalidField("first_available_sequence"));
478 }
479
480 let requested_start = self
481 .after_event_sequence
482 .map(|sequence| sequence.saturating_add(1))
483 .unwrap_or(0);
484 let expected_gap = requested_start < self.latest_sequence_exclusive
485 && self
486 .first_available_sequence
487 .is_none_or(|first| requested_start < first);
488 if self.retention_gap != expected_gap {
489 return Err(AgentProtocolError::InvalidField("retention_gap"));
490 }
491
492 let mut previous: Option<(u64, u64)> = None;
493 for event in &self.events {
494 event.validate_for(&self.identity)?;
495 if event.occurred_at_ms > self.observed_at_ms
496 || previous.is_some_and(|(sequence, timestamp)| {
497 event.sequence != sequence.saturating_add(1) || event.occurred_at_ms < timestamp
498 })
499 || event.sequence >= self.latest_sequence_exclusive
500 {
501 return Err(AgentProtocolError::InvalidField("events"));
502 }
503 previous = Some((event.sequence, event.occurred_at_ms));
504 }
505
506 if let Some(first) = self.events.first() {
507 if (!self.retention_gap && first.sequence != requested_start)
508 || (self.retention_gap && self.first_available_sequence != Some(first.sequence))
509 || self
510 .first_available_sequence
511 .is_some_and(|available| first.sequence < available)
512 {
513 return Err(AgentProtocolError::InvalidField("events"));
514 }
515 } else if self.retention_gap && self.first_available_sequence.is_some() {
516 return Err(AgentProtocolError::InvalidField("events"));
517 }
518 let expected_next = self
519 .events
520 .last()
521 .map(|event| event.sequence)
522 .or(self.after_event_sequence);
523 if self.next_after_event_sequence != expected_next {
524 return Err(AgentProtocolError::InvalidField(
525 "next_after_event_sequence",
526 ));
527 }
528 if self.has_more {
529 if self.events.last().is_none_or(|event| {
530 event.sequence.saturating_add(1) >= self.latest_sequence_exclusive
531 }) {
532 return Err(AgentProtocolError::InvalidField("has_more"));
533 }
534 } else if let Some(last) = self.events.last() {
535 if last.sequence.saturating_add(1) != self.latest_sequence_exclusive {
536 return Err(AgentProtocolError::InvalidField("has_more"));
537 }
538 }
539 let encoded = serde_json::to_vec(self).map_err(|_| AgentProtocolError::Encoding)?;
540 if encoded.len() > AGENT_PROTOCOL_MAX_EVENT_PAGE_BYTES {
541 return Err(AgentProtocolError::InvalidField("events"));
542 }
543 Ok(())
544 }
545
546 pub fn first_sequence(&self) -> Option<u64> {
547 self.events.first().map(|event| event.sequence)
548 }
549
550 pub fn last_sequence(&self) -> Option<u64> {
551 self.events.last().map(|event| event.sequence)
552 }
553
554 pub fn digest(&self) -> Result<String, AgentProtocolError> {
555 digest_validated(self, || self.validate())
556 }
557}
558
559fn validate_schema(value: &str, expected: &str) -> Result<(), AgentProtocolError> {
560 if value == expected {
561 Ok(())
562 } else {
563 Err(AgentProtocolError::UnsupportedSchema)
564 }
565}
566
567fn validate_id(field: &'static str, value: &str) -> Result<(), AgentProtocolError> {
568 validate_single_line(field, value, AGENT_PROTOCOL_MAX_ID_BYTES)
569}
570
571fn validate_single_line(
572 field: &'static str,
573 value: &str,
574 max: usize,
575) -> Result<(), AgentProtocolError> {
576 if value.trim().is_empty()
577 || value.len() > max
578 || value.contains('\0')
579 || value.contains(['\r', '\n'])
580 {
581 Err(AgentProtocolError::InvalidField(field))
582 } else {
583 Ok(())
584 }
585}
586
587pub(crate) fn validate_lower_sha256(
588 field: &'static str,
589 value: &str,
590) -> Result<(), AgentProtocolError> {
591 let valid = value.strip_prefix("sha256:").is_some_and(|hex| {
592 hex.len() == 64
593 && hex
594 .bytes()
595 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
596 });
597 if valid {
598 Ok(())
599 } else {
600 Err(AgentProtocolError::InvalidField(field))
601 }
602}
603
604fn validate_json_size(
605 field: &'static str,
606 value: &serde_json::Value,
607 max: usize,
608) -> Result<(), AgentProtocolError> {
609 let encoded = serde_json::to_vec(value).map_err(|_| AgentProtocolError::Encoding)?;
610 if encoded.len() > max {
611 Err(AgentProtocolError::InvalidField(field))
612 } else {
613 Ok(())
614 }
615}
616
617fn digest_validated<T: Serialize>(
618 value: &T,
619 validate: impl FnOnce() -> Result<(), AgentProtocolError>,
620) -> Result<String, AgentProtocolError> {
621 validate()?;
622 let encoded = serde_json::to_vec(value).map_err(|_| AgentProtocolError::Encoding)?;
623 Ok(format!("sha256:{:x}", Sha256::digest(encoded)))
624}