Skip to main content

oris_agent_contract/
lib.rs

1//! Proposal-only runtime contract for external agents.
2
3use serde::{Deserialize, Serialize};
4
5pub const A2A_PROTOCOL_NAME: &str = "oris.a2a";
6pub const A2A_PROTOCOL_VERSION: &str = "0.1.0-experimental";
7pub const A2A_PROTOCOL_VERSION_V1: &str = "1.0.0";
8pub const A2A_SUPPORTED_PROTOCOL_VERSIONS: [&str; 2] =
9    [A2A_PROTOCOL_VERSION_V1, A2A_PROTOCOL_VERSION];
10
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
12pub struct A2aProtocol {
13    pub name: String,
14    pub version: String,
15}
16
17impl A2aProtocol {
18    pub fn current() -> Self {
19        Self {
20            name: A2A_PROTOCOL_NAME.to_string(),
21            version: A2A_PROTOCOL_VERSION.to_string(),
22        }
23    }
24}
25
26#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
27pub enum A2aCapability {
28    Coordination,
29    MutationProposal,
30    ReplayFeedback,
31    SupervisedDevloop,
32    EvolutionPublish,
33    EvolutionFetch,
34    EvolutionRevoke,
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
38pub struct A2aHandshakeRequest {
39    pub agent_id: String,
40    pub role: AgentRole,
41    pub capability_level: AgentCapabilityLevel,
42    pub supported_protocols: Vec<A2aProtocol>,
43    pub advertised_capabilities: Vec<A2aCapability>,
44}
45
46impl A2aHandshakeRequest {
47    pub fn supports_protocol_version(&self, version: &str) -> bool {
48        self.supported_protocols
49            .iter()
50            .any(|protocol| protocol.name == A2A_PROTOCOL_NAME && protocol.version == version)
51    }
52
53    pub fn supports_current_protocol(&self) -> bool {
54        self.supports_protocol_version(A2A_PROTOCOL_VERSION)
55    }
56
57    pub fn negotiate_supported_protocol(&self) -> Option<A2aProtocol> {
58        for version in A2A_SUPPORTED_PROTOCOL_VERSIONS {
59            if self.supports_protocol_version(version) {
60                return Some(A2aProtocol {
61                    name: A2A_PROTOCOL_NAME.to_string(),
62                    version: version.to_string(),
63                });
64            }
65        }
66        None
67    }
68}
69
70#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
71pub struct A2aHandshakeResponse {
72    pub accepted: bool,
73    pub negotiated_protocol: Option<A2aProtocol>,
74    pub enabled_capabilities: Vec<A2aCapability>,
75    pub message: Option<String>,
76    pub error: Option<A2aErrorEnvelope>,
77}
78
79impl A2aHandshakeResponse {
80    pub fn accept(enabled_capabilities: Vec<A2aCapability>) -> Self {
81        Self {
82            accepted: true,
83            negotiated_protocol: Some(A2aProtocol::current()),
84            enabled_capabilities,
85            message: Some("handshake accepted".to_string()),
86            error: None,
87        }
88    }
89
90    pub fn reject(code: A2aErrorCode, message: impl Into<String>, details: Option<String>) -> Self {
91        Self {
92            accepted: false,
93            negotiated_protocol: None,
94            enabled_capabilities: Vec::new(),
95            message: Some("handshake rejected".to_string()),
96            error: Some(A2aErrorEnvelope {
97                code,
98                message: message.into(),
99                retriable: true,
100                details,
101            }),
102        }
103    }
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
107pub enum A2aTaskLifecycleState {
108    Queued,
109    Running,
110    Succeeded,
111    Failed,
112    Cancelled,
113}
114
115#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
116pub struct A2aTaskLifecycleEvent {
117    pub task_id: String,
118    pub state: A2aTaskLifecycleState,
119    pub summary: String,
120    pub updated_at_ms: u64,
121    pub error: Option<A2aErrorEnvelope>,
122}
123
124pub const A2A_TASK_SESSION_PROTOCOL_VERSION: &str = A2A_PROTOCOL_VERSION;
125
126#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
127pub enum A2aTaskSessionState {
128    Started,
129    Dispatched,
130    InProgress,
131    Completed,
132    Failed,
133    Cancelled,
134}
135
136#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
137pub struct A2aTaskSessionStartRequest {
138    pub sender_id: String,
139    pub protocol_version: String,
140    pub task_id: String,
141    pub task_summary: String,
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
145pub struct A2aTaskSessionDispatchRequest {
146    pub sender_id: String,
147    pub protocol_version: String,
148    pub dispatch_id: String,
149    pub summary: String,
150}
151
152#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
153pub struct A2aTaskSessionProgressRequest {
154    pub sender_id: String,
155    pub protocol_version: String,
156    pub progress_pct: u8,
157    pub summary: String,
158    pub retryable: bool,
159    pub retry_after_ms: Option<u64>,
160}
161
162#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
163pub struct A2aTaskSessionCompletionRequest {
164    pub sender_id: String,
165    pub protocol_version: String,
166    pub terminal_state: A2aTaskLifecycleState,
167    pub summary: String,
168    pub retryable: bool,
169    pub retry_after_ms: Option<u64>,
170    pub failure_code: Option<A2aErrorCode>,
171    pub failure_details: Option<String>,
172    pub used_capsule: bool,
173    pub capsule_id: Option<String>,
174    pub reasoning_steps_avoided: u64,
175    pub fallback_reason: Option<String>,
176    pub task_class_id: String,
177    pub task_label: String,
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
181pub struct A2aTaskSessionProgressItem {
182    pub progress_pct: u8,
183    pub summary: String,
184    pub retryable: bool,
185    pub retry_after_ms: Option<u64>,
186    pub updated_at_ms: u64,
187}
188
189#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
190pub struct A2aTaskSessionAck {
191    pub session_id: String,
192    pub task_id: String,
193    pub state: A2aTaskSessionState,
194    pub summary: String,
195    pub retryable: bool,
196    pub retry_after_ms: Option<u64>,
197    pub updated_at_ms: u64,
198}
199
200#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
201pub struct A2aTaskSessionResult {
202    pub terminal_state: A2aTaskLifecycleState,
203    pub summary: String,
204    pub retryable: bool,
205    pub retry_after_ms: Option<u64>,
206    pub failure_code: Option<A2aErrorCode>,
207    pub failure_details: Option<String>,
208    pub replay_feedback: ReplayFeedback,
209}
210
211#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
212pub struct A2aTaskSessionCompletionResponse {
213    pub ack: A2aTaskSessionAck,
214    pub result: A2aTaskSessionResult,
215}
216
217#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
218pub struct A2aTaskSessionSnapshot {
219    pub session_id: String,
220    pub sender_id: String,
221    pub task_id: String,
222    pub protocol_version: String,
223    pub state: A2aTaskSessionState,
224    pub created_at_ms: u64,
225    pub updated_at_ms: u64,
226    pub dispatch_ids: Vec<String>,
227    pub progress: Vec<A2aTaskSessionProgressItem>,
228    pub result: Option<A2aTaskSessionResult>,
229}
230
231#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
232pub enum A2aErrorCode {
233    UnsupportedProtocol,
234    UnsupportedCapability,
235    ValidationFailed,
236    AuthorizationDenied,
237    Timeout,
238    Internal,
239}
240
241#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
242pub struct A2aErrorEnvelope {
243    pub code: A2aErrorCode,
244    pub message: String,
245    pub retriable: bool,
246    pub details: Option<String>,
247}
248
249#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
250pub enum AgentCapabilityLevel {
251    A0,
252    A1,
253    A2,
254    A3,
255    A4,
256}
257
258#[derive(Clone, Debug, Serialize, Deserialize)]
259pub enum ProposalTarget {
260    WorkspaceRoot,
261    Paths(Vec<String>),
262}
263
264#[derive(Clone, Debug, Serialize, Deserialize)]
265pub struct AgentTask {
266    pub id: String,
267    pub description: String,
268}
269
270#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
271pub enum AgentRole {
272    Planner,
273    Coder,
274    Repair,
275    Optimizer,
276}
277
278#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
279pub enum CoordinationPrimitive {
280    Sequential,
281    Parallel,
282    Conditional,
283}
284
285#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
286pub struct CoordinationTask {
287    pub id: String,
288    pub role: AgentRole,
289    pub description: String,
290    pub depends_on: Vec<String>,
291}
292
293#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
294pub struct CoordinationMessage {
295    pub from_role: AgentRole,
296    pub to_role: AgentRole,
297    pub task_id: String,
298    pub content: String,
299}
300
301#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
302pub struct CoordinationPlan {
303    pub root_goal: String,
304    pub primitive: CoordinationPrimitive,
305    pub tasks: Vec<CoordinationTask>,
306    pub timeout_ms: u64,
307    pub max_retries: u32,
308}
309
310#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
311pub struct CoordinationResult {
312    pub completed_tasks: Vec<String>,
313    pub failed_tasks: Vec<String>,
314    pub messages: Vec<CoordinationMessage>,
315    pub summary: String,
316}
317
318#[derive(Clone, Debug, Serialize, Deserialize)]
319pub struct MutationProposal {
320    pub intent: String,
321    pub files: Vec<String>,
322    pub expected_effect: String,
323}
324
325#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
326pub struct ExecutionFeedback {
327    pub accepted: bool,
328    pub asset_state: Option<String>,
329    pub summary: String,
330}
331
332#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
333pub enum ReplayPlannerDirective {
334    SkipPlanner,
335    PlanFallback,
336}
337
338#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
339pub struct ReplayFeedback {
340    pub used_capsule: bool,
341    pub capsule_id: Option<String>,
342    pub planner_directive: ReplayPlannerDirective,
343    pub reasoning_steps_avoided: u64,
344    pub fallback_reason: Option<String>,
345    pub task_class_id: String,
346    pub task_label: String,
347    pub summary: String,
348}
349
350#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
351pub enum BoundedTaskClass {
352    DocsSingleFile,
353}
354
355#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
356pub struct HumanApproval {
357    pub approved: bool,
358    pub approver: Option<String>,
359    pub note: Option<String>,
360}
361
362#[derive(Clone, Debug, Serialize, Deserialize)]
363pub struct SupervisedDevloopRequest {
364    pub task: AgentTask,
365    pub proposal: MutationProposal,
366    pub approval: HumanApproval,
367}
368
369#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
370pub enum SupervisedDevloopStatus {
371    AwaitingApproval,
372    RejectedByPolicy,
373    Executed,
374}
375
376#[derive(Clone, Debug, Serialize, Deserialize)]
377pub struct SupervisedDevloopOutcome {
378    pub task_id: String,
379    pub task_class: Option<BoundedTaskClass>,
380    pub status: SupervisedDevloopStatus,
381    pub execution_feedback: Option<ExecutionFeedback>,
382    pub summary: String,
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    fn handshake_request_with_versions(versions: &[&str]) -> A2aHandshakeRequest {
390        A2aHandshakeRequest {
391            agent_id: "agent-test".into(),
392            role: AgentRole::Planner,
393            capability_level: AgentCapabilityLevel::A2,
394            supported_protocols: versions
395                .iter()
396                .map(|version| A2aProtocol {
397                    name: A2A_PROTOCOL_NAME.into(),
398                    version: (*version).into(),
399                })
400                .collect(),
401            advertised_capabilities: vec![A2aCapability::Coordination],
402        }
403    }
404
405    #[test]
406    fn negotiate_supported_protocol_prefers_v1_when_available() {
407        let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION, A2A_PROTOCOL_VERSION_V1]);
408        let negotiated = req
409            .negotiate_supported_protocol()
410            .expect("expected protocol negotiation success");
411        assert_eq!(negotiated.name, A2A_PROTOCOL_NAME);
412        assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION_V1);
413    }
414
415    #[test]
416    fn negotiate_supported_protocol_falls_back_to_experimental() {
417        let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION]);
418        let negotiated = req
419            .negotiate_supported_protocol()
420            .expect("expected protocol negotiation success");
421        assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION);
422    }
423
424    #[test]
425    fn negotiate_supported_protocol_returns_none_without_overlap() {
426        let req = handshake_request_with_versions(&["0.0.1"]);
427        assert!(req.negotiate_supported_protocol().is_none());
428    }
429}