1use 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 reason_code: Option<ReplayFallbackReasonCode>,
177 pub repair_hint: Option<String>,
178 pub next_action: Option<ReplayFallbackNextAction>,
179 pub confidence: Option<u8>,
180 pub task_class_id: String,
181 pub task_label: String,
182}
183
184#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
185pub struct A2aTaskSessionProgressItem {
186 pub progress_pct: u8,
187 pub summary: String,
188 pub retryable: bool,
189 pub retry_after_ms: Option<u64>,
190 pub updated_at_ms: u64,
191}
192
193#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
194pub struct A2aTaskSessionAck {
195 pub session_id: String,
196 pub task_id: String,
197 pub state: A2aTaskSessionState,
198 pub summary: String,
199 pub retryable: bool,
200 pub retry_after_ms: Option<u64>,
201 pub updated_at_ms: u64,
202}
203
204#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
205pub struct A2aTaskSessionResult {
206 pub terminal_state: A2aTaskLifecycleState,
207 pub summary: String,
208 pub retryable: bool,
209 pub retry_after_ms: Option<u64>,
210 pub failure_code: Option<A2aErrorCode>,
211 pub failure_details: Option<String>,
212 pub replay_feedback: ReplayFeedback,
213}
214
215#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
216pub struct A2aTaskSessionCompletionResponse {
217 pub ack: A2aTaskSessionAck,
218 pub result: A2aTaskSessionResult,
219}
220
221#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
222pub struct A2aTaskSessionSnapshot {
223 pub session_id: String,
224 pub sender_id: String,
225 pub task_id: String,
226 pub protocol_version: String,
227 pub state: A2aTaskSessionState,
228 pub created_at_ms: u64,
229 pub updated_at_ms: u64,
230 pub dispatch_ids: Vec<String>,
231 pub progress: Vec<A2aTaskSessionProgressItem>,
232 pub result: Option<A2aTaskSessionResult>,
233}
234
235#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
236pub enum A2aErrorCode {
237 UnsupportedProtocol,
238 UnsupportedCapability,
239 ValidationFailed,
240 AuthorizationDenied,
241 Timeout,
242 Internal,
243}
244
245#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
246pub struct A2aErrorEnvelope {
247 pub code: A2aErrorCode,
248 pub message: String,
249 pub retriable: bool,
250 pub details: Option<String>,
251}
252
253#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
254pub enum AgentCapabilityLevel {
255 A0,
256 A1,
257 A2,
258 A3,
259 A4,
260}
261
262#[derive(Clone, Debug, Serialize, Deserialize)]
263pub enum ProposalTarget {
264 WorkspaceRoot,
265 Paths(Vec<String>),
266}
267
268#[derive(Clone, Debug, Serialize, Deserialize)]
269pub struct AgentTask {
270 pub id: String,
271 pub description: String,
272}
273
274#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
275pub enum AgentRole {
276 Planner,
277 Coder,
278 Repair,
279 Optimizer,
280}
281
282#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
283pub enum CoordinationPrimitive {
284 Sequential,
285 Parallel,
286 Conditional,
287}
288
289#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
290pub struct CoordinationTask {
291 pub id: String,
292 pub role: AgentRole,
293 pub description: String,
294 pub depends_on: Vec<String>,
295}
296
297#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
298pub struct CoordinationMessage {
299 pub from_role: AgentRole,
300 pub to_role: AgentRole,
301 pub task_id: String,
302 pub content: String,
303}
304
305#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
306pub struct CoordinationPlan {
307 pub root_goal: String,
308 pub primitive: CoordinationPrimitive,
309 pub tasks: Vec<CoordinationTask>,
310 pub timeout_ms: u64,
311 pub max_retries: u32,
312}
313
314#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
315pub struct CoordinationResult {
316 pub completed_tasks: Vec<String>,
317 pub failed_tasks: Vec<String>,
318 pub messages: Vec<CoordinationMessage>,
319 pub summary: String,
320}
321
322#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
323pub struct MutationProposal {
324 pub intent: String,
325 pub files: Vec<String>,
326 pub expected_effect: String,
327}
328
329#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
330#[serde(rename_all = "snake_case")]
331pub enum MutationProposalContractReasonCode {
332 Accepted,
333 CandidateRejected,
334 MissingTargetFiles,
335 OutOfBoundsPath,
336 UnsupportedTaskClass,
337 ValidationBudgetExceeded,
338 ExpectedEvidenceMissing,
339 UnknownFailClosed,
340}
341
342#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
343#[serde(rename_all = "snake_case")]
344pub enum MutationProposalEvidence {
345 HumanApproval,
346 BoundedScope,
347 ValidationPass,
348 ExecutionAudit,
349}
350
351#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
352pub struct MutationProposalValidationBudget {
353 pub max_diff_bytes: usize,
354 pub max_changed_lines: usize,
355 pub validation_timeout_ms: u64,
356}
357
358#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
359pub struct ExecutionFeedback {
360 pub accepted: bool,
361 pub asset_state: Option<String>,
362 pub summary: String,
363}
364
365#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
366pub enum ReplayPlannerDirective {
367 SkipPlanner,
368 PlanFallback,
369}
370
371#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
372#[serde(rename_all = "snake_case")]
373pub enum ReplayFallbackReasonCode {
374 NoCandidateAfterSelect,
375 ScoreBelowThreshold,
376 CandidateHasNoCapsule,
377 MutationPayloadMissing,
378 PatchApplyFailed,
379 ValidationFailed,
380 UnmappedFallbackReason,
381}
382
383#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
384#[serde(rename_all = "snake_case")]
385pub enum ReplayFallbackNextAction {
386 PlanFromScratch,
387 ValidateSignalsThenPlan,
388 RebuildCapsule,
389 RegenerateMutationPayload,
390 RebasePatchAndRetry,
391 RepairAndRevalidate,
392 EscalateFailClosed,
393}
394
395#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
396pub struct ReplayFallbackContract {
397 pub reason_code: ReplayFallbackReasonCode,
398 pub fallback_reason: String,
399 pub repair_hint: String,
400 pub next_action: ReplayFallbackNextAction,
401 pub confidence: u8,
403}
404
405pub fn infer_replay_fallback_reason_code(reason: &str) -> Option<ReplayFallbackReasonCode> {
406 let normalized = reason.trim().to_ascii_lowercase();
407 if normalized.is_empty() {
408 return None;
409 }
410 if normalized == "no_candidate_after_select" || normalized.contains("no matching gene") {
411 return Some(ReplayFallbackReasonCode::NoCandidateAfterSelect);
412 }
413 if normalized == "score_below_threshold" || normalized.contains("below replay threshold") {
414 return Some(ReplayFallbackReasonCode::ScoreBelowThreshold);
415 }
416 if normalized == "candidate_has_no_capsule" || normalized.contains("has no capsule") {
417 return Some(ReplayFallbackReasonCode::CandidateHasNoCapsule);
418 }
419 if normalized == "mutation_payload_missing" || normalized.contains("payload missing") {
420 return Some(ReplayFallbackReasonCode::MutationPayloadMissing);
421 }
422 if normalized == "patch_apply_failed" || normalized.contains("patch apply failed") {
423 return Some(ReplayFallbackReasonCode::PatchApplyFailed);
424 }
425 if normalized == "validation_failed" || normalized.contains("validation failed") {
426 return Some(ReplayFallbackReasonCode::ValidationFailed);
427 }
428 None
429}
430
431pub fn normalize_replay_fallback_contract(
432 planner_directive: &ReplayPlannerDirective,
433 fallback_reason: Option<&str>,
434 reason_code: Option<ReplayFallbackReasonCode>,
435 repair_hint: Option<&str>,
436 next_action: Option<ReplayFallbackNextAction>,
437 confidence: Option<u8>,
438) -> Option<ReplayFallbackContract> {
439 if !matches!(planner_directive, ReplayPlannerDirective::PlanFallback) {
440 return None;
441 }
442
443 let normalized_reason = normalize_optional_text(fallback_reason);
444 let normalized_repair_hint = normalize_optional_text(repair_hint);
445 let mut resolved_reason_code = reason_code
446 .or_else(|| {
447 normalized_reason
448 .as_deref()
449 .and_then(infer_replay_fallback_reason_code)
450 })
451 .unwrap_or(ReplayFallbackReasonCode::UnmappedFallbackReason);
452 let mut defaults = replay_fallback_defaults(&resolved_reason_code);
453
454 let mut force_fail_closed = false;
455 if let Some(provided_action) = next_action {
456 if provided_action != defaults.next_action {
457 resolved_reason_code = ReplayFallbackReasonCode::UnmappedFallbackReason;
458 defaults = replay_fallback_defaults(&resolved_reason_code);
459 force_fail_closed = true;
460 }
461 }
462
463 Some(ReplayFallbackContract {
464 reason_code: resolved_reason_code,
465 fallback_reason: normalized_reason.unwrap_or_else(|| defaults.fallback_reason.to_string()),
466 repair_hint: normalized_repair_hint.unwrap_or_else(|| defaults.repair_hint.to_string()),
467 next_action: if force_fail_closed {
468 defaults.next_action
469 } else {
470 next_action.unwrap_or(defaults.next_action)
471 },
472 confidence: if force_fail_closed {
473 defaults.confidence
474 } else {
475 confidence.unwrap_or(defaults.confidence).min(100)
476 },
477 })
478}
479
480#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
481pub struct ReplayFeedback {
482 pub used_capsule: bool,
483 pub capsule_id: Option<String>,
484 pub planner_directive: ReplayPlannerDirective,
485 pub reasoning_steps_avoided: u64,
486 pub fallback_reason: Option<String>,
487 pub reason_code: Option<ReplayFallbackReasonCode>,
488 pub repair_hint: Option<String>,
489 pub next_action: Option<ReplayFallbackNextAction>,
490 pub confidence: Option<u8>,
491 pub task_class_id: String,
492 pub task_label: String,
493 pub summary: String,
494}
495
496#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
497#[serde(rename_all = "snake_case")]
498pub enum MutationNeededFailureReasonCode {
499 PolicyDenied,
500 ValidationFailed,
501 UnsafePatch,
502 Timeout,
503 MutationPayloadMissing,
504 UnknownFailClosed,
505}
506
507#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
508#[serde(rename_all = "snake_case")]
509pub enum MutationNeededRecoveryAction {
510 NarrowScopeAndRetry,
511 RepairAndRevalidate,
512 ProduceSafePatch,
513 ReduceExecutionBudget,
514 RegenerateMutationPayload,
515 EscalateFailClosed,
516}
517
518#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
519pub struct MutationNeededFailureContract {
520 pub reason_code: MutationNeededFailureReasonCode,
521 pub failure_reason: String,
522 pub recovery_hint: String,
523 pub recovery_action: MutationNeededRecoveryAction,
524 pub fail_closed: bool,
525}
526
527pub fn infer_mutation_needed_failure_reason_code(
528 reason: &str,
529) -> Option<MutationNeededFailureReasonCode> {
530 let normalized = reason.trim().to_ascii_lowercase();
531 if normalized.is_empty() {
532 return None;
533 }
534 if normalized.contains("mutation payload missing") || normalized == "mutation_payload_missing" {
535 return Some(MutationNeededFailureReasonCode::MutationPayloadMissing);
536 }
537 if normalized.contains("command timed out") || normalized.contains(" timeout") {
538 return Some(MutationNeededFailureReasonCode::Timeout);
539 }
540 if normalized.contains("patch rejected")
541 || normalized.contains("patch apply failed")
542 || normalized.contains("target violation")
543 || normalized.contains("unsafe patch")
544 {
545 return Some(MutationNeededFailureReasonCode::UnsafePatch);
546 }
547 if normalized.contains("validation failed") {
548 return Some(MutationNeededFailureReasonCode::ValidationFailed);
549 }
550 if normalized.contains("command denied by policy")
551 || normalized.contains("rejected task")
552 || normalized.contains("unsupported task outside the bounded scope")
553 || normalized.contains("budget exceeds bounded policy")
554 {
555 return Some(MutationNeededFailureReasonCode::PolicyDenied);
556 }
557 None
558}
559
560pub fn normalize_mutation_needed_failure_contract(
561 failure_reason: Option<&str>,
562 reason_code: Option<MutationNeededFailureReasonCode>,
563) -> MutationNeededFailureContract {
564 let normalized_reason = normalize_optional_text(failure_reason);
565 let resolved_reason_code = reason_code
566 .or_else(|| {
567 normalized_reason
568 .as_deref()
569 .and_then(infer_mutation_needed_failure_reason_code)
570 })
571 .unwrap_or(MutationNeededFailureReasonCode::UnknownFailClosed);
572 let defaults = mutation_needed_failure_defaults(&resolved_reason_code);
573
574 MutationNeededFailureContract {
575 reason_code: resolved_reason_code,
576 failure_reason: normalized_reason.unwrap_or_else(|| defaults.failure_reason.to_string()),
577 recovery_hint: defaults.recovery_hint.to_string(),
578 recovery_action: defaults.recovery_action,
579 fail_closed: true,
580 }
581}
582
583fn normalize_optional_text(value: Option<&str>) -> Option<String> {
584 let trimmed = value?.trim();
585 if trimmed.is_empty() {
586 None
587 } else {
588 Some(trimmed.to_string())
589 }
590}
591
592#[derive(Clone, Copy)]
593struct ReplayFallbackDefaults {
594 fallback_reason: &'static str,
595 repair_hint: &'static str,
596 next_action: ReplayFallbackNextAction,
597 confidence: u8,
598}
599
600fn replay_fallback_defaults(reason_code: &ReplayFallbackReasonCode) -> ReplayFallbackDefaults {
601 match reason_code {
602 ReplayFallbackReasonCode::NoCandidateAfterSelect => ReplayFallbackDefaults {
603 fallback_reason: "no matching gene",
604 repair_hint:
605 "No reusable capsule matched deterministic signals; run planner for a minimal patch.",
606 next_action: ReplayFallbackNextAction::PlanFromScratch,
607 confidence: 92,
608 },
609 ReplayFallbackReasonCode::ScoreBelowThreshold => ReplayFallbackDefaults {
610 fallback_reason: "candidate score below replay threshold",
611 repair_hint:
612 "Best replay candidate is below threshold; validate task signals and re-plan.",
613 next_action: ReplayFallbackNextAction::ValidateSignalsThenPlan,
614 confidence: 86,
615 },
616 ReplayFallbackReasonCode::CandidateHasNoCapsule => ReplayFallbackDefaults {
617 fallback_reason: "candidate gene has no capsule",
618 repair_hint: "Matched gene has no executable capsule; rebuild capsule from planner output.",
619 next_action: ReplayFallbackNextAction::RebuildCapsule,
620 confidence: 80,
621 },
622 ReplayFallbackReasonCode::MutationPayloadMissing => ReplayFallbackDefaults {
623 fallback_reason: "mutation payload missing from store",
624 repair_hint:
625 "Mutation payload is missing; regenerate and persist a minimal mutation payload.",
626 next_action: ReplayFallbackNextAction::RegenerateMutationPayload,
627 confidence: 76,
628 },
629 ReplayFallbackReasonCode::PatchApplyFailed => ReplayFallbackDefaults {
630 fallback_reason: "replay patch apply failed",
631 repair_hint: "Replay patch cannot be applied cleanly; rebase patch and retry planning.",
632 next_action: ReplayFallbackNextAction::RebasePatchAndRetry,
633 confidence: 68,
634 },
635 ReplayFallbackReasonCode::ValidationFailed => ReplayFallbackDefaults {
636 fallback_reason: "replay validation failed",
637 repair_hint: "Replay validation failed; produce a repair mutation and re-run validation.",
638 next_action: ReplayFallbackNextAction::RepairAndRevalidate,
639 confidence: 64,
640 },
641 ReplayFallbackReasonCode::UnmappedFallbackReason => ReplayFallbackDefaults {
642 fallback_reason: "unmapped replay fallback reason",
643 repair_hint:
644 "Fallback reason is unmapped; fail closed and require explicit planner intervention.",
645 next_action: ReplayFallbackNextAction::EscalateFailClosed,
646 confidence: 0,
647 },
648 }
649}
650
651#[derive(Clone, Copy)]
652struct MutationNeededFailureDefaults {
653 failure_reason: &'static str,
654 recovery_hint: &'static str,
655 recovery_action: MutationNeededRecoveryAction,
656}
657
658fn mutation_needed_failure_defaults(
659 reason_code: &MutationNeededFailureReasonCode,
660) -> MutationNeededFailureDefaults {
661 match reason_code {
662 MutationNeededFailureReasonCode::PolicyDenied => MutationNeededFailureDefaults {
663 failure_reason: "mutation needed denied by bounded execution policy",
664 recovery_hint:
665 "Narrow changed scope to the approved docs boundary and re-run with explicit approval.",
666 recovery_action: MutationNeededRecoveryAction::NarrowScopeAndRetry,
667 },
668 MutationNeededFailureReasonCode::ValidationFailed => MutationNeededFailureDefaults {
669 failure_reason: "mutation needed validation failed",
670 recovery_hint:
671 "Repair mutation and re-run validation to produce a deterministic pass before capture.",
672 recovery_action: MutationNeededRecoveryAction::RepairAndRevalidate,
673 },
674 MutationNeededFailureReasonCode::UnsafePatch => MutationNeededFailureDefaults {
675 failure_reason: "mutation needed rejected unsafe patch",
676 recovery_hint:
677 "Generate a safer minimal diff confined to approved paths and verify patch applicability.",
678 recovery_action: MutationNeededRecoveryAction::ProduceSafePatch,
679 },
680 MutationNeededFailureReasonCode::Timeout => MutationNeededFailureDefaults {
681 failure_reason: "mutation needed execution timed out",
682 recovery_hint:
683 "Reduce execution budget or split the mutation into smaller steps before retrying.",
684 recovery_action: MutationNeededRecoveryAction::ReduceExecutionBudget,
685 },
686 MutationNeededFailureReasonCode::MutationPayloadMissing => MutationNeededFailureDefaults {
687 failure_reason: "mutation payload missing from store",
688 recovery_hint: "Regenerate and persist mutation payload before retrying mutation-needed.",
689 recovery_action: MutationNeededRecoveryAction::RegenerateMutationPayload,
690 },
691 MutationNeededFailureReasonCode::UnknownFailClosed => MutationNeededFailureDefaults {
692 failure_reason: "mutation needed failed with unmapped reason",
693 recovery_hint:
694 "Unknown failure class; fail closed and require explicit maintainer triage before retry.",
695 recovery_action: MutationNeededRecoveryAction::EscalateFailClosed,
696 },
697 }
698}
699
700#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
701pub enum BoundedTaskClass {
702 DocsSingleFile,
703 DocsMultiFile,
704 CargoDepUpgrade,
707 LintFix,
710}
711
712#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
713pub struct MutationProposalScope {
714 pub task_class: BoundedTaskClass,
715 pub target_files: Vec<String>,
716}
717
718#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
719pub struct SelfEvolutionMutationProposalContract {
720 pub mutation_proposal: MutationProposal,
721 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub proposal_scope: Option<MutationProposalScope>,
723 pub validation_budget: MutationProposalValidationBudget,
724 pub approval_required: bool,
725 pub expected_evidence: Vec<MutationProposalEvidence>,
726 pub summary: String,
727 #[serde(default, skip_serializing_if = "Option::is_none")]
728 pub failure_reason: Option<String>,
729 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub recovery_hint: Option<String>,
731 pub reason_code: MutationProposalContractReasonCode,
732 pub fail_closed: bool,
733}
734
735#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
736pub struct SelfEvolutionCandidateIntakeRequest {
737 pub issue_number: u64,
738 pub title: String,
739 pub body: String,
740 #[serde(default)]
741 pub labels: Vec<String>,
742 pub state: String,
743 #[serde(default)]
744 pub candidate_hint_paths: Vec<String>,
745}
746
747#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
749#[serde(rename_all = "snake_case")]
750pub enum AutonomousCandidateSource {
751 CiFailure,
752 TestRegression,
753 CompileRegression,
754 LintRegression,
755 RuntimeIncident,
756}
757
758#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
760#[serde(rename_all = "snake_case")]
761pub enum AutonomousIntakeReasonCode {
762 Accepted,
763 UnsupportedSignalClass,
764 AmbiguousSignal,
765 DuplicateCandidate,
766 UnknownFailClosed,
767}
768
769#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
772pub struct DiscoveredCandidate {
773 pub dedupe_key: String,
775 pub candidate_source: AutonomousCandidateSource,
777 pub candidate_class: Option<BoundedTaskClass>,
779 pub signals: Vec<String>,
781 pub accepted: bool,
783 pub reason_code: AutonomousIntakeReasonCode,
785 pub summary: String,
787 #[serde(default, skip_serializing_if = "Option::is_none")]
788 pub failure_reason: Option<String>,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
790 pub recovery_hint: Option<String>,
791 pub fail_closed: bool,
793}
794
795#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
797pub struct AutonomousIntakeInput {
798 pub source_id: String,
800 pub candidate_source: AutonomousCandidateSource,
802 pub raw_signals: Vec<String>,
804}
805
806#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
809pub struct AutonomousIntakeOutput {
810 pub candidates: Vec<DiscoveredCandidate>,
811 pub accepted_count: usize,
812 pub denied_count: usize,
813}
814
815#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
819#[serde(rename_all = "snake_case")]
820pub enum AutonomousRiskTier {
821 Low,
823 Medium,
825 High,
827}
828
829#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
831#[serde(rename_all = "snake_case")]
832pub enum AutonomousPlanReasonCode {
833 Approved,
834 DeniedHighRisk,
835 DeniedLowFeasibility,
836 DeniedUnsupportedClass,
837 DeniedNoEvidence,
838 UnknownFailClosed,
839}
840
841#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
843pub struct AutonomousDenialCondition {
844 pub reason_code: AutonomousPlanReasonCode,
845 pub description: String,
846 pub recovery_hint: String,
847}
848
849#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
851pub struct AutonomousTaskPlan {
852 pub plan_id: String,
854 pub dedupe_key: String,
856 pub task_class: Option<BoundedTaskClass>,
858 pub risk_tier: AutonomousRiskTier,
860 pub feasibility_score: u8,
862 pub validation_budget: u8,
864 pub expected_evidence: Vec<String>,
866 pub approved: bool,
868 pub reason_code: AutonomousPlanReasonCode,
870 pub summary: String,
872 #[serde(default, skip_serializing_if = "Option::is_none")]
874 pub denial_condition: Option<AutonomousDenialCondition>,
875 pub fail_closed: bool,
877}
878
879pub fn approve_autonomous_task_plan(
880 plan_id: impl Into<String>,
881 dedupe_key: impl Into<String>,
882 task_class: BoundedTaskClass,
883 risk_tier: AutonomousRiskTier,
884 feasibility_score: u8,
885 validation_budget: u8,
886 expected_evidence: Vec<String>,
887 summary: Option<&str>,
888) -> AutonomousTaskPlan {
889 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
890 format!("autonomous task plan approved for {task_class:?} at {risk_tier:?} risk")
891 });
892 AutonomousTaskPlan {
893 plan_id: plan_id.into(),
894 dedupe_key: dedupe_key.into(),
895 task_class: Some(task_class),
896 risk_tier,
897 feasibility_score,
898 validation_budget,
899 expected_evidence,
900 approved: true,
901 reason_code: AutonomousPlanReasonCode::Approved,
902 summary,
903 denial_condition: None,
904 fail_closed: false,
905 }
906}
907
908pub fn deny_autonomous_task_plan(
909 plan_id: impl Into<String>,
910 dedupe_key: impl Into<String>,
911 risk_tier: AutonomousRiskTier,
912 reason_code: AutonomousPlanReasonCode,
913) -> AutonomousTaskPlan {
914 let (description, recovery_hint) = match reason_code {
915 AutonomousPlanReasonCode::DeniedHighRisk => (
916 "task plan denied because risk tier is too high for autonomous execution",
917 "reduce blast radius by scoping the change to a single bounded file before retrying",
918 ),
919 AutonomousPlanReasonCode::DeniedLowFeasibility => (
920 "task plan denied because feasibility score is below the policy threshold",
921 "provide stronger evidence or narrow the task scope before retrying",
922 ),
923 AutonomousPlanReasonCode::DeniedUnsupportedClass => (
924 "task plan denied because task class is not supported for autonomous planning",
925 "route this task class through the supervised planning path instead",
926 ),
927 AutonomousPlanReasonCode::DeniedNoEvidence => (
928 "task plan denied because no evidence was available to assess feasibility",
929 "ensure signals and candidate class are populated before planning",
930 ),
931 AutonomousPlanReasonCode::UnknownFailClosed => (
932 "task plan failed with an unmapped reason; fail closed",
933 "require explicit maintainer triage before retry",
934 ),
935 AutonomousPlanReasonCode::Approved => (
936 "unexpected approved reason on deny path",
937 "use approve_autonomous_task_plan for approved outcomes",
938 ),
939 };
940 let summary = format!("autonomous task plan denied [{reason_code:?}]: {description}");
941 AutonomousTaskPlan {
942 plan_id: plan_id.into(),
943 dedupe_key: dedupe_key.into(),
944 task_class: None,
945 risk_tier,
946 feasibility_score: 0,
947 validation_budget: 0,
948 expected_evidence: Vec::new(),
949 approved: false,
950 reason_code,
951 summary,
952 denial_condition: Some(AutonomousDenialCondition {
953 reason_code,
954 description: description.to_string(),
955 recovery_hint: recovery_hint.to_string(),
956 }),
957 fail_closed: true,
958 }
959}
960
961#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
965#[serde(rename_all = "snake_case")]
966pub enum AutonomousApprovalMode {
967 AutoApproved,
969 RequiresHumanReview,
971}
972
973#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
975#[serde(rename_all = "snake_case")]
976pub enum AutonomousProposalReasonCode {
977 Proposed,
978 DeniedPlanNotApproved,
979 DeniedNoTargetScope,
980 DeniedWeakEvidence,
981 DeniedOutOfBounds,
982 UnknownFailClosed,
983}
984
985#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
987pub struct AutonomousProposalScope {
988 pub target_paths: Vec<String>,
990 pub scope_rationale: String,
992 pub max_files: u8,
994}
995
996#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
998pub struct AutonomousMutationProposal {
999 pub proposal_id: String,
1001 pub plan_id: String,
1003 pub dedupe_key: String,
1005 pub scope: Option<AutonomousProposalScope>,
1007 pub expected_evidence: Vec<String>,
1009 pub rollback_conditions: Vec<String>,
1011 pub approval_mode: AutonomousApprovalMode,
1013 pub proposed: bool,
1015 pub reason_code: AutonomousProposalReasonCode,
1017 pub summary: String,
1019 #[serde(default, skip_serializing_if = "Option::is_none")]
1021 pub denial_condition: Option<AutonomousDenialCondition>,
1022 pub fail_closed: bool,
1024}
1025
1026pub fn approve_autonomous_mutation_proposal(
1028 proposal_id: impl Into<String>,
1029 plan_id: impl Into<String>,
1030 dedupe_key: impl Into<String>,
1031 scope: AutonomousProposalScope,
1032 expected_evidence: Vec<String>,
1033 rollback_conditions: Vec<String>,
1034 approval_mode: AutonomousApprovalMode,
1035 summary: Option<&str>,
1036) -> AutonomousMutationProposal {
1037 let dedupe_key = dedupe_key.into();
1038 let summary = summary
1039 .and_then(|s| {
1040 if s.trim().is_empty() {
1041 None
1042 } else {
1043 Some(s.to_string())
1044 }
1045 })
1046 .unwrap_or_else(|| format!("autonomous mutation proposal approved for {dedupe_key}"));
1047 AutonomousMutationProposal {
1048 proposal_id: proposal_id.into(),
1049 plan_id: plan_id.into(),
1050 dedupe_key,
1051 scope: Some(scope),
1052 expected_evidence,
1053 rollback_conditions,
1054 approval_mode,
1055 proposed: true,
1056 reason_code: AutonomousProposalReasonCode::Proposed,
1057 summary,
1058 denial_condition: None,
1059 fail_closed: false,
1060 }
1061}
1062
1063pub fn deny_autonomous_mutation_proposal(
1065 proposal_id: impl Into<String>,
1066 plan_id: impl Into<String>,
1067 dedupe_key: impl Into<String>,
1068 reason_code: AutonomousProposalReasonCode,
1069) -> AutonomousMutationProposal {
1070 let (description, recovery_hint) = match reason_code {
1071 AutonomousProposalReasonCode::DeniedPlanNotApproved => (
1072 "source plan was not approved; cannot generate proposal",
1073 "ensure the autonomous task plan is approved before proposing a mutation",
1074 ),
1075 AutonomousProposalReasonCode::DeniedNoTargetScope => (
1076 "no bounded target scope could be determined for this task class",
1077 "verify that the task class maps to a known set of target paths",
1078 ),
1079 AutonomousProposalReasonCode::DeniedWeakEvidence => (
1080 "the expected evidence set is empty or insufficient",
1081 "strengthen evidence requirements before reattempting proposal",
1082 ),
1083 AutonomousProposalReasonCode::DeniedOutOfBounds => (
1084 "proposal would mutate files outside of bounded policy scope",
1085 "restrict targets to explicitly allowed paths and retry",
1086 ),
1087 AutonomousProposalReasonCode::UnknownFailClosed => (
1088 "proposal generation failed with an unmapped reason; fail closed",
1089 "require explicit maintainer triage before retry",
1090 ),
1091 AutonomousProposalReasonCode::Proposed => (
1092 "unexpected proposed reason on deny path",
1093 "use approve_autonomous_mutation_proposal for proposed outcomes",
1094 ),
1095 };
1096 let dedupe_key = dedupe_key.into();
1097 let summary = format!("autonomous mutation proposal denied [{reason_code:?}]: {description}");
1098 AutonomousMutationProposal {
1099 proposal_id: proposal_id.into(),
1100 plan_id: plan_id.into(),
1101 dedupe_key,
1102 scope: None,
1103 expected_evidence: Vec::new(),
1104 rollback_conditions: Vec::new(),
1105 approval_mode: AutonomousApprovalMode::RequiresHumanReview,
1106 proposed: false,
1107 reason_code,
1108 summary,
1109 denial_condition: Some(AutonomousDenialCondition {
1110 reason_code: AutonomousPlanReasonCode::UnknownFailClosed,
1111 description: description.to_string(),
1112 recovery_hint: recovery_hint.to_string(),
1113 }),
1114 fail_closed: true,
1115 }
1116}
1117
1118#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1119#[serde(rename_all = "snake_case")]
1120pub enum SelfEvolutionSelectionReasonCode {
1121 Accepted,
1122 IssueClosed,
1123 MissingEvolutionLabel,
1124 MissingFeatureLabel,
1125 ExcludedByLabel,
1126 UnsupportedCandidateScope,
1127 UnknownFailClosed,
1128}
1129
1130#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1131pub struct SelfEvolutionSelectionDecision {
1132 pub issue_number: u64,
1133 pub selected: bool,
1134 #[serde(default, skip_serializing_if = "Option::is_none")]
1135 pub candidate_class: Option<BoundedTaskClass>,
1136 pub summary: String,
1137 #[serde(default, skip_serializing_if = "Option::is_none")]
1138 pub reason_code: Option<SelfEvolutionSelectionReasonCode>,
1139 #[serde(default, skip_serializing_if = "Option::is_none")]
1140 pub failure_reason: Option<String>,
1141 #[serde(default, skip_serializing_if = "Option::is_none")]
1142 pub recovery_hint: Option<String>,
1143 pub fail_closed: bool,
1144}
1145
1146#[derive(Clone, Copy)]
1147struct SelfEvolutionSelectionDefaults {
1148 failure_reason: &'static str,
1149 recovery_hint: &'static str,
1150}
1151
1152fn self_evolution_selection_defaults(
1153 reason_code: &SelfEvolutionSelectionReasonCode,
1154) -> Option<SelfEvolutionSelectionDefaults> {
1155 match reason_code {
1156 SelfEvolutionSelectionReasonCode::Accepted => None,
1157 SelfEvolutionSelectionReasonCode::IssueClosed => Some(SelfEvolutionSelectionDefaults {
1158 failure_reason: "self-evolution candidate rejected because the issue is closed",
1159 recovery_hint: "Reopen the issue or choose an active open issue before retrying selection.",
1160 }),
1161 SelfEvolutionSelectionReasonCode::MissingEvolutionLabel => {
1162 Some(SelfEvolutionSelectionDefaults {
1163 failure_reason: "self-evolution candidate rejected because the issue is missing area/evolution",
1164 recovery_hint:
1165 "Add the area/evolution label or choose an issue already scoped to self-evolution.",
1166 })
1167 }
1168 SelfEvolutionSelectionReasonCode::MissingFeatureLabel => {
1169 Some(SelfEvolutionSelectionDefaults {
1170 failure_reason: "self-evolution candidate rejected because the issue is missing type/feature",
1171 recovery_hint:
1172 "Add the type/feature label or narrow the issue to a bounded feature slice before retrying.",
1173 })
1174 }
1175 SelfEvolutionSelectionReasonCode::ExcludedByLabel => Some(SelfEvolutionSelectionDefaults {
1176 failure_reason: "self-evolution candidate rejected by an excluded issue label",
1177 recovery_hint:
1178 "Remove the excluded label or choose a non-duplicate, non-invalid, actionable issue.",
1179 }),
1180 SelfEvolutionSelectionReasonCode::UnsupportedCandidateScope => {
1181 Some(SelfEvolutionSelectionDefaults {
1182 failure_reason:
1183 "self-evolution candidate rejected because the hinted file scope is outside the bounded docs policy",
1184 recovery_hint:
1185 "Narrow candidate paths to the approved docs/*.md boundary before retrying selection.",
1186 })
1187 }
1188 SelfEvolutionSelectionReasonCode::UnknownFailClosed => Some(SelfEvolutionSelectionDefaults {
1189 failure_reason: "self-evolution candidate failed with an unmapped selection reason",
1190 recovery_hint: "Unknown selection failure; fail closed and require explicit maintainer triage before retry.",
1191 }),
1192 }
1193}
1194
1195pub fn accept_self_evolution_selection_decision(
1196 issue_number: u64,
1197 candidate_class: BoundedTaskClass,
1198 summary: Option<&str>,
1199) -> SelfEvolutionSelectionDecision {
1200 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
1201 format!("selected GitHub issue #{issue_number} as a bounded self-evolution candidate")
1202 });
1203 SelfEvolutionSelectionDecision {
1204 issue_number,
1205 selected: true,
1206 candidate_class: Some(candidate_class),
1207 summary,
1208 reason_code: Some(SelfEvolutionSelectionReasonCode::Accepted),
1209 failure_reason: None,
1210 recovery_hint: None,
1211 fail_closed: false,
1212 }
1213}
1214
1215pub fn reject_self_evolution_selection_decision(
1216 issue_number: u64,
1217 reason_code: SelfEvolutionSelectionReasonCode,
1218 failure_reason: Option<&str>,
1219 summary: Option<&str>,
1220) -> SelfEvolutionSelectionDecision {
1221 let defaults = self_evolution_selection_defaults(&reason_code)
1222 .unwrap_or(SelfEvolutionSelectionDefaults {
1223 failure_reason: "self-evolution candidate rejected",
1224 recovery_hint:
1225 "Review candidate selection inputs and retry within the bounded self-evolution policy.",
1226 });
1227 let failure_reason = normalize_optional_text(failure_reason)
1228 .unwrap_or_else(|| defaults.failure_reason.to_string());
1229 let reason_code_key = match reason_code {
1230 SelfEvolutionSelectionReasonCode::Accepted => "accepted",
1231 SelfEvolutionSelectionReasonCode::IssueClosed => "issue_closed",
1232 SelfEvolutionSelectionReasonCode::MissingEvolutionLabel => "missing_evolution_label",
1233 SelfEvolutionSelectionReasonCode::MissingFeatureLabel => "missing_feature_label",
1234 SelfEvolutionSelectionReasonCode::ExcludedByLabel => "excluded_by_label",
1235 SelfEvolutionSelectionReasonCode::UnsupportedCandidateScope => {
1236 "unsupported_candidate_scope"
1237 }
1238 SelfEvolutionSelectionReasonCode::UnknownFailClosed => "unknown_fail_closed",
1239 };
1240 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
1241 format!(
1242 "rejected GitHub issue #{issue_number} as a self-evolution candidate [{reason_code_key}]"
1243 )
1244 });
1245
1246 SelfEvolutionSelectionDecision {
1247 issue_number,
1248 selected: false,
1249 candidate_class: None,
1250 summary,
1251 reason_code: Some(reason_code),
1252 failure_reason: Some(failure_reason),
1253 recovery_hint: Some(defaults.recovery_hint.to_string()),
1254 fail_closed: true,
1255 }
1256}
1257
1258pub fn accept_discovered_candidate(
1259 dedupe_key: impl Into<String>,
1260 candidate_source: AutonomousCandidateSource,
1261 candidate_class: BoundedTaskClass,
1262 signals: Vec<String>,
1263 summary: Option<&str>,
1264) -> DiscoveredCandidate {
1265 let summary = normalize_optional_text(summary)
1266 .unwrap_or_else(|| format!("accepted autonomous candidate from {candidate_source:?}"));
1267 DiscoveredCandidate {
1268 dedupe_key: dedupe_key.into(),
1269 candidate_source,
1270 candidate_class: Some(candidate_class),
1271 signals,
1272 accepted: true,
1273 reason_code: AutonomousIntakeReasonCode::Accepted,
1274 summary,
1275 failure_reason: None,
1276 recovery_hint: None,
1277 fail_closed: false,
1278 }
1279}
1280
1281pub fn deny_discovered_candidate(
1282 dedupe_key: impl Into<String>,
1283 candidate_source: AutonomousCandidateSource,
1284 signals: Vec<String>,
1285 reason_code: AutonomousIntakeReasonCode,
1286) -> DiscoveredCandidate {
1287 let (failure_reason, recovery_hint) = match reason_code {
1288 AutonomousIntakeReasonCode::UnsupportedSignalClass => (
1289 "signal class is not supported by the bounded evolution policy",
1290 "review supported candidate signal classes and filter input before retry",
1291 ),
1292 AutonomousIntakeReasonCode::AmbiguousSignal => (
1293 "signals do not map to a unique bounded candidate class",
1294 "provide more specific signal tokens or triage manually before resubmitting",
1295 ),
1296 AutonomousIntakeReasonCode::DuplicateCandidate => (
1297 "an equivalent candidate has already been discovered in this intake window",
1298 "deduplicate signals before resubmitting or check the existing candidate queue",
1299 ),
1300 AutonomousIntakeReasonCode::UnknownFailClosed => (
1301 "candidate intake failed with an unmapped reason; fail closed",
1302 "require explicit maintainer triage before retry",
1303 ),
1304 AutonomousIntakeReasonCode::Accepted => (
1305 "unexpected accepted reason on deny path",
1306 "use accept_discovered_candidate for accepted outcomes",
1307 ),
1308 };
1309 let summary =
1310 format!("denied autonomous candidate from {candidate_source:?}: {failure_reason}");
1311 DiscoveredCandidate {
1312 dedupe_key: dedupe_key.into(),
1313 candidate_source,
1314 candidate_class: None,
1315 signals,
1316 accepted: false,
1317 reason_code,
1318 summary,
1319 failure_reason: Some(failure_reason.to_string()),
1320 recovery_hint: Some(recovery_hint.to_string()),
1321 fail_closed: true,
1322 }
1323}
1324
1325#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1326pub struct HumanApproval {
1327 pub approved: bool,
1328 pub approver: Option<String>,
1329 pub note: Option<String>,
1330}
1331
1332#[derive(Clone, Debug, Serialize, Deserialize)]
1333pub struct SupervisedDevloopRequest {
1334 pub task: AgentTask,
1335 pub proposal: MutationProposal,
1336 pub approval: HumanApproval,
1337}
1338
1339#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1340pub enum SupervisedDevloopStatus {
1341 AwaitingApproval,
1342 RejectedByPolicy,
1343 FailedClosed,
1344 Executed,
1345}
1346
1347#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1348#[serde(rename_all = "snake_case")]
1349pub enum SupervisedDeliveryStatus {
1350 Prepared,
1351 Denied,
1352}
1353
1354#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1355#[serde(rename_all = "snake_case")]
1356pub enum SupervisedDeliveryApprovalState {
1357 Approved,
1358 MissingExplicitApproval,
1359}
1360
1361#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1362#[serde(rename_all = "snake_case")]
1363pub enum SupervisedDeliveryReasonCode {
1364 DeliveryPrepared,
1365 AwaitingApproval,
1366 DeliveryEvidenceMissing,
1367 ValidationEvidenceMissing,
1368 UnsupportedTaskScope,
1369 InconsistentDeliveryEvidence,
1370 UnknownFailClosed,
1371}
1372
1373#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1374pub struct SupervisedDeliveryContract {
1375 pub delivery_summary: String,
1376 #[serde(default, skip_serializing_if = "Option::is_none")]
1377 pub branch_name: Option<String>,
1378 #[serde(default, skip_serializing_if = "Option::is_none")]
1379 pub pr_title: Option<String>,
1380 #[serde(default, skip_serializing_if = "Option::is_none")]
1381 pub pr_summary: Option<String>,
1382 pub delivery_status: SupervisedDeliveryStatus,
1383 pub approval_state: SupervisedDeliveryApprovalState,
1384 pub reason_code: SupervisedDeliveryReasonCode,
1385 #[serde(default)]
1386 pub fail_closed: bool,
1387 #[serde(default, skip_serializing_if = "Option::is_none")]
1388 pub recovery_hint: Option<String>,
1389}
1390
1391#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1392#[serde(rename_all = "snake_case")]
1393pub enum SupervisedExecutionDecision {
1394 AwaitingApproval,
1395 ReplayHit,
1396 PlannerFallback,
1397 RejectedByPolicy,
1398 FailedClosed,
1399}
1400
1401#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1402#[serde(rename_all = "snake_case")]
1403pub enum SupervisedValidationOutcome {
1404 NotRun,
1405 Passed,
1406 FailedClosed,
1407}
1408
1409#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1410#[serde(rename_all = "snake_case")]
1411pub enum SupervisedExecutionReasonCode {
1412 AwaitingHumanApproval,
1413 ReplayHit,
1414 ReplayFallback,
1415 PolicyDenied,
1416 ValidationFailed,
1417 UnsafePatch,
1418 Timeout,
1419 MutationPayloadMissing,
1420 UnknownFailClosed,
1421}
1422#[derive(Clone, Debug, Serialize, Deserialize)]
1423pub struct SupervisedDevloopOutcome {
1424 pub task_id: String,
1425 pub task_class: Option<BoundedTaskClass>,
1426 pub status: SupervisedDevloopStatus,
1427 pub execution_decision: SupervisedExecutionDecision,
1428 #[serde(default, skip_serializing_if = "Option::is_none")]
1429 pub replay_outcome: Option<ReplayFeedback>,
1430 #[serde(default, skip_serializing_if = "Option::is_none")]
1431 pub fallback_reason: Option<String>,
1432 pub validation_outcome: SupervisedValidationOutcome,
1433 pub evidence_summary: String,
1434 #[serde(default, skip_serializing_if = "Option::is_none")]
1435 pub reason_code: Option<SupervisedExecutionReasonCode>,
1436 #[serde(default, skip_serializing_if = "Option::is_none")]
1437 pub recovery_hint: Option<String>,
1438 pub execution_feedback: Option<ExecutionFeedback>,
1439 #[serde(default, skip_serializing_if = "Option::is_none")]
1440 pub failure_contract: Option<MutationNeededFailureContract>,
1441 pub summary: String,
1442}
1443
1444#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1445#[serde(rename_all = "snake_case")]
1446pub enum SelfEvolutionAuditConsistencyResult {
1447 Consistent,
1448 Inconsistent,
1449}
1450
1451#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1452#[serde(rename_all = "snake_case")]
1453pub enum SelfEvolutionAcceptanceGateReasonCode {
1454 Accepted,
1455 MissingSelectionEvidence,
1456 MissingProposalEvidence,
1457 MissingApprovalEvidence,
1458 MissingExecutionEvidence,
1459 MissingDeliveryEvidence,
1460 InconsistentReasonCodeMatrix,
1461 UnknownFailClosed,
1462}
1463
1464#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1465pub struct SelfEvolutionApprovalEvidence {
1466 pub approval_required: bool,
1467 pub approved: bool,
1468 #[serde(default, skip_serializing_if = "Option::is_none")]
1469 pub approver: Option<String>,
1470}
1471
1472#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1473pub struct SelfEvolutionDeliveryOutcome {
1474 pub delivery_status: SupervisedDeliveryStatus,
1475 pub approval_state: SupervisedDeliveryApprovalState,
1476 pub reason_code: SupervisedDeliveryReasonCode,
1477}
1478
1479#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1480pub struct SelfEvolutionReasonCodeMatrix {
1481 #[serde(default, skip_serializing_if = "Option::is_none")]
1482 pub selection_reason_code: Option<SelfEvolutionSelectionReasonCode>,
1483 pub proposal_reason_code: MutationProposalContractReasonCode,
1484 #[serde(default, skip_serializing_if = "Option::is_none")]
1485 pub execution_reason_code: Option<SupervisedExecutionReasonCode>,
1486 pub delivery_reason_code: SupervisedDeliveryReasonCode,
1487}
1488
1489#[derive(Clone, Debug, Serialize, Deserialize)]
1490pub struct SelfEvolutionAcceptanceGateInput {
1491 pub selection_decision: SelfEvolutionSelectionDecision,
1492 pub proposal_contract: SelfEvolutionMutationProposalContract,
1493 pub supervised_request: SupervisedDevloopRequest,
1494 pub execution_outcome: SupervisedDevloopOutcome,
1495 pub delivery_contract: SupervisedDeliveryContract,
1496}
1497
1498#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1499pub struct SelfEvolutionAcceptanceGateContract {
1500 pub acceptance_gate_summary: String,
1501 pub audit_consistency_result: SelfEvolutionAuditConsistencyResult,
1502 pub approval_evidence: SelfEvolutionApprovalEvidence,
1503 pub delivery_outcome: SelfEvolutionDeliveryOutcome,
1504 pub reason_code_matrix: SelfEvolutionReasonCodeMatrix,
1505 pub fail_closed: bool,
1506 pub reason_code: SelfEvolutionAcceptanceGateReasonCode,
1507 #[serde(default, skip_serializing_if = "Option::is_none")]
1508 pub recovery_hint: Option<String>,
1509}
1510
1511#[cfg(test)]
1512mod tests {
1513 use super::*;
1514
1515 fn handshake_request_with_versions(versions: &[&str]) -> A2aHandshakeRequest {
1516 A2aHandshakeRequest {
1517 agent_id: "agent-test".into(),
1518 role: AgentRole::Planner,
1519 capability_level: AgentCapabilityLevel::A2,
1520 supported_protocols: versions
1521 .iter()
1522 .map(|version| A2aProtocol {
1523 name: A2A_PROTOCOL_NAME.into(),
1524 version: (*version).into(),
1525 })
1526 .collect(),
1527 advertised_capabilities: vec![A2aCapability::Coordination],
1528 }
1529 }
1530
1531 #[test]
1532 fn negotiate_supported_protocol_prefers_v1_when_available() {
1533 let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION, A2A_PROTOCOL_VERSION_V1]);
1534 let negotiated = req
1535 .negotiate_supported_protocol()
1536 .expect("expected protocol negotiation success");
1537 assert_eq!(negotiated.name, A2A_PROTOCOL_NAME);
1538 assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION_V1);
1539 }
1540
1541 #[test]
1542 fn negotiate_supported_protocol_falls_back_to_experimental() {
1543 let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION]);
1544 let negotiated = req
1545 .negotiate_supported_protocol()
1546 .expect("expected protocol negotiation success");
1547 assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION);
1548 }
1549
1550 #[test]
1551 fn negotiate_supported_protocol_returns_none_without_overlap() {
1552 let req = handshake_request_with_versions(&["0.0.1"]);
1553 assert!(req.negotiate_supported_protocol().is_none());
1554 }
1555
1556 #[test]
1557 fn normalize_replay_fallback_contract_maps_known_reason() {
1558 let contract = normalize_replay_fallback_contract(
1559 &ReplayPlannerDirective::PlanFallback,
1560 Some("no matching gene"),
1561 None,
1562 None,
1563 None,
1564 None,
1565 )
1566 .expect("contract should exist");
1567
1568 assert_eq!(
1569 contract.reason_code,
1570 ReplayFallbackReasonCode::NoCandidateAfterSelect
1571 );
1572 assert_eq!(
1573 contract.next_action,
1574 ReplayFallbackNextAction::PlanFromScratch
1575 );
1576 assert_eq!(contract.confidence, 92);
1577 }
1578
1579 #[test]
1580 fn normalize_replay_fallback_contract_fails_closed_for_unknown_reason() {
1581 let contract = normalize_replay_fallback_contract(
1582 &ReplayPlannerDirective::PlanFallback,
1583 Some("something unexpected"),
1584 None,
1585 None,
1586 None,
1587 None,
1588 )
1589 .expect("contract should exist");
1590
1591 assert_eq!(
1592 contract.reason_code,
1593 ReplayFallbackReasonCode::UnmappedFallbackReason
1594 );
1595 assert_eq!(
1596 contract.next_action,
1597 ReplayFallbackNextAction::EscalateFailClosed
1598 );
1599 assert_eq!(contract.confidence, 0);
1600 }
1601
1602 #[test]
1603 fn normalize_replay_fallback_contract_rejects_conflicting_next_action() {
1604 let contract = normalize_replay_fallback_contract(
1605 &ReplayPlannerDirective::PlanFallback,
1606 Some("replay validation failed"),
1607 Some(ReplayFallbackReasonCode::ValidationFailed),
1608 None,
1609 Some(ReplayFallbackNextAction::PlanFromScratch),
1610 Some(88),
1611 )
1612 .expect("contract should exist");
1613
1614 assert_eq!(
1615 contract.reason_code,
1616 ReplayFallbackReasonCode::UnmappedFallbackReason
1617 );
1618 assert_eq!(
1619 contract.next_action,
1620 ReplayFallbackNextAction::EscalateFailClosed
1621 );
1622 assert_eq!(contract.confidence, 0);
1623 }
1624
1625 #[test]
1626 fn normalize_mutation_needed_failure_contract_maps_policy_denied() {
1627 let contract = normalize_mutation_needed_failure_contract(
1628 Some("supervised devloop rejected task because it is outside bounded scope"),
1629 None,
1630 );
1631
1632 assert_eq!(
1633 contract.reason_code,
1634 MutationNeededFailureReasonCode::PolicyDenied
1635 );
1636 assert_eq!(
1637 contract.recovery_action,
1638 MutationNeededRecoveryAction::NarrowScopeAndRetry
1639 );
1640 assert!(contract.fail_closed);
1641 }
1642
1643 #[test]
1644 fn normalize_mutation_needed_failure_contract_maps_timeout() {
1645 let contract = normalize_mutation_needed_failure_contract(
1646 Some("command timed out: git apply --check patch.diff"),
1647 None,
1648 );
1649
1650 assert_eq!(
1651 contract.reason_code,
1652 MutationNeededFailureReasonCode::Timeout
1653 );
1654 assert_eq!(
1655 contract.recovery_action,
1656 MutationNeededRecoveryAction::ReduceExecutionBudget
1657 );
1658 assert!(contract.fail_closed);
1659 }
1660
1661 #[test]
1662 fn normalize_mutation_needed_failure_contract_fails_closed_for_unknown_reason() {
1663 let contract =
1664 normalize_mutation_needed_failure_contract(Some("unexpected runner panic"), None);
1665
1666 assert_eq!(
1667 contract.reason_code,
1668 MutationNeededFailureReasonCode::UnknownFailClosed
1669 );
1670 assert_eq!(
1671 contract.recovery_action,
1672 MutationNeededRecoveryAction::EscalateFailClosed
1673 );
1674 assert!(contract.fail_closed);
1675 }
1676
1677 #[test]
1678 fn reject_self_evolution_selection_decision_maps_closed_issue_defaults() {
1679 let decision = reject_self_evolution_selection_decision(
1680 234,
1681 SelfEvolutionSelectionReasonCode::IssueClosed,
1682 None,
1683 None,
1684 );
1685
1686 assert!(!decision.selected);
1687 assert_eq!(decision.issue_number, 234);
1688 assert_eq!(
1689 decision.reason_code,
1690 Some(SelfEvolutionSelectionReasonCode::IssueClosed)
1691 );
1692 assert!(decision.fail_closed);
1693 assert!(decision
1694 .failure_reason
1695 .as_deref()
1696 .is_some_and(|reason| reason.contains("closed")));
1697 assert!(decision.recovery_hint.is_some());
1698 }
1699
1700 #[test]
1701 fn accept_self_evolution_selection_decision_marks_candidate_selected() {
1702 let decision =
1703 accept_self_evolution_selection_decision(235, BoundedTaskClass::DocsSingleFile, None);
1704
1705 assert!(decision.selected);
1706 assert_eq!(decision.issue_number, 235);
1707 assert_eq!(
1708 decision.candidate_class,
1709 Some(BoundedTaskClass::DocsSingleFile)
1710 );
1711 assert_eq!(
1712 decision.reason_code,
1713 Some(SelfEvolutionSelectionReasonCode::Accepted)
1714 );
1715 assert!(!decision.fail_closed);
1716 assert_eq!(decision.failure_reason, None);
1717 assert_eq!(decision.recovery_hint, None);
1718 }
1719}
1720
1721#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1723pub enum HubTrustTier {
1724 Full,
1726 ReadOnly,
1728}
1729
1730#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
1732pub enum HubOperationClass {
1733 Hello,
1734 Fetch,
1735 Publish,
1736 Revoke,
1737 TaskClaim,
1738 TaskComplete,
1739 WorkerRegister,
1740 Recipe,
1741 Session,
1742 Dispute,
1743 Swarm,
1744}
1745
1746impl HubOperationClass {
1747 pub fn is_read_only(&self) -> bool {
1749 matches!(self, HubOperationClass::Hello | HubOperationClass::Fetch)
1750 }
1751}
1752
1753#[derive(Clone, Debug, Serialize, Deserialize)]
1755pub struct HubProfile {
1756 pub hub_id: String,
1757 pub base_url: String,
1758 pub trust_tier: HubTrustTier,
1759 pub priority: u32,
1761 pub health_url: Option<String>,
1763}
1764
1765impl HubProfile {
1766 pub fn allows_operation(&self, operation: &HubOperationClass) -> bool {
1768 match &self.trust_tier {
1769 HubTrustTier::Full => true,
1770 HubTrustTier::ReadOnly => operation.is_read_only(),
1771 }
1772 }
1773}
1774
1775#[derive(Clone, Debug)]
1777pub struct HubSelectionPolicy {
1778 pub allowed_tiers_for_operation: Vec<(HubOperationClass, Vec<HubTrustTier>)>,
1780 pub default_allowed_tiers: Vec<HubTrustTier>,
1782}
1783
1784impl Default for HubSelectionPolicy {
1785 fn default() -> Self {
1786 Self {
1787 allowed_tiers_for_operation: vec![
1788 (
1789 HubOperationClass::Hello,
1790 vec![HubTrustTier::Full, HubTrustTier::ReadOnly],
1791 ),
1792 (
1793 HubOperationClass::Fetch,
1794 vec![HubTrustTier::Full, HubTrustTier::ReadOnly],
1795 ),
1796 (HubOperationClass::Publish, vec![HubTrustTier::Full]),
1798 (HubOperationClass::Revoke, vec![HubTrustTier::Full]),
1799 (HubOperationClass::TaskClaim, vec![HubTrustTier::Full]),
1800 (HubOperationClass::TaskComplete, vec![HubTrustTier::Full]),
1801 (HubOperationClass::WorkerRegister, vec![HubTrustTier::Full]),
1802 (HubOperationClass::Recipe, vec![HubTrustTier::Full]),
1803 (HubOperationClass::Session, vec![HubTrustTier::Full]),
1804 (HubOperationClass::Dispute, vec![HubTrustTier::Full]),
1805 (HubOperationClass::Swarm, vec![HubTrustTier::Full]),
1806 ],
1807 default_allowed_tiers: vec![HubTrustTier::Full],
1808 }
1809 }
1810}
1811
1812impl HubSelectionPolicy {
1813 pub fn allowed_tiers(&self, operation: &HubOperationClass) -> &[HubTrustTier] {
1815 self.allowed_tiers_for_operation
1816 .iter()
1817 .find(|(op, _)| op == operation)
1818 .map(|(_, tiers)| tiers.as_slice())
1819 .unwrap_or(&self.default_allowed_tiers)
1820 }
1821}