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)]
1128#[serde(rename_all = "snake_case")]
1129pub enum TaskEquivalenceClass {
1130 DocumentationEdit,
1133 StaticAnalysisFix,
1135 DependencyManifestUpdate,
1137 Unclassified,
1139}
1140
1141#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1144pub struct EquivalenceExplanation {
1145 pub task_equivalence_class: TaskEquivalenceClass,
1147 pub rationale: String,
1149 pub matching_features: Vec<String>,
1151 pub replay_match_confidence: u8,
1153}
1154
1155#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1157#[serde(rename_all = "snake_case")]
1158pub enum SemanticReplayReasonCode {
1159 EquivalenceMatchApproved,
1161 LowConfidenceDenied,
1163 NoEquivalenceClassMatch,
1165 EquivalenceClassNotAllowed,
1168 UnknownFailClosed,
1170}
1171
1172#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1178pub struct SemanticReplayDecision {
1179 pub evaluation_id: String,
1181 pub task_id: String,
1183 pub replay_decision: bool,
1185 pub equivalence_explanation: Option<EquivalenceExplanation>,
1187 pub reason_code: SemanticReplayReasonCode,
1189 pub summary: String,
1191 pub fail_closed: bool,
1194}
1195
1196pub fn approve_semantic_replay(
1198 evaluation_id: impl Into<String>,
1199 task_id: impl Into<String>,
1200 explanation: EquivalenceExplanation,
1201) -> SemanticReplayDecision {
1202 let summary = format!(
1203 "semantic replay approved [equivalence_class={:?}, confidence={}]",
1204 explanation.task_equivalence_class, explanation.replay_match_confidence
1205 );
1206 SemanticReplayDecision {
1207 evaluation_id: evaluation_id.into(),
1208 task_id: task_id.into(),
1209 replay_decision: true,
1210 reason_code: SemanticReplayReasonCode::EquivalenceMatchApproved,
1211 equivalence_explanation: Some(explanation),
1212 summary,
1213 fail_closed: false,
1214 }
1215}
1216
1217pub fn deny_semantic_replay(
1219 evaluation_id: impl Into<String>,
1220 task_id: impl Into<String>,
1221 reason_code: SemanticReplayReasonCode,
1222 context: impl Into<String>,
1223) -> SemanticReplayDecision {
1224 let context: String = context.into();
1225 let summary = format!("semantic replay denied [{reason_code:?}]: {context}");
1226 SemanticReplayDecision {
1227 evaluation_id: evaluation_id.into(),
1228 task_id: task_id.into(),
1229 replay_decision: false,
1230 equivalence_explanation: None,
1231 reason_code,
1232 summary,
1233 fail_closed: true,
1234 }
1235}
1236
1237#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1243#[serde(rename_all = "snake_case")]
1244pub enum AutonomousPrLaneStatus {
1245 PrReady,
1247 Denied,
1249}
1250
1251#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1253#[serde(rename_all = "snake_case")]
1254pub enum PrLaneApprovalState {
1255 ClassApproved,
1257 ClassNotApproved,
1259}
1260
1261#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1263#[serde(rename_all = "snake_case")]
1264pub enum AutonomousPrLaneReasonCode {
1265 ApprovedForAutonomousPr,
1267 TaskClassNotApproved,
1269 PatchEvidenceMissing,
1271 ValidationEvidenceMissing,
1273 RiskTierTooHigh,
1275 UnknownFailClosed,
1277}
1278
1279#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1281pub struct PrEvidenceBundle {
1282 pub patch_summary: String,
1284 pub validation_passed: bool,
1286 pub audit_trail: Vec<String>,
1288}
1289
1290#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1292pub struct AutonomousPrLaneDecision {
1293 pub pr_lane_id: String,
1295 pub delivery_summary: String,
1297 #[serde(default, skip_serializing_if = "Option::is_none")]
1300 pub branch_name: Option<String>,
1301 #[serde(default, skip_serializing_if = "Option::is_none")]
1303 pub pr_payload: Option<String>,
1304 #[serde(default, skip_serializing_if = "Option::is_none")]
1306 pub evidence_bundle: Option<PrEvidenceBundle>,
1307 pub pr_ready: bool,
1309 pub delivery_status: AutonomousPrLaneStatus,
1311 pub approval_state: PrLaneApprovalState,
1313 pub reason_code: AutonomousPrLaneReasonCode,
1315 pub fail_closed: bool,
1318}
1319
1320pub fn approve_autonomous_pr_lane(
1322 pr_lane_id: impl Into<String>,
1323 task_id: impl Into<String>,
1324 branch_name: impl Into<String>,
1325 evidence_bundle: PrEvidenceBundle,
1326) -> AutonomousPrLaneDecision {
1327 let task_id: String = task_id.into();
1328 let branch = branch_name.into();
1329 let pr_payload = format!("Autonomous PR for task {task_id} on branch {branch}");
1330 let delivery_summary =
1331 format!("autonomous PR lane approved for task {task_id}: branch {branch} ready");
1332 AutonomousPrLaneDecision {
1333 pr_lane_id: pr_lane_id.into(),
1334 delivery_summary,
1335 branch_name: Some(branch),
1336 pr_payload: Some(pr_payload),
1337 evidence_bundle: Some(evidence_bundle),
1338 pr_ready: true,
1339 delivery_status: AutonomousPrLaneStatus::PrReady,
1340 approval_state: PrLaneApprovalState::ClassApproved,
1341 reason_code: AutonomousPrLaneReasonCode::ApprovedForAutonomousPr,
1342 fail_closed: false,
1343 }
1344}
1345
1346pub fn deny_autonomous_pr_lane(
1348 pr_lane_id: impl Into<String>,
1349 task_id: impl Into<String>,
1350 reason_code: AutonomousPrLaneReasonCode,
1351 detail: impl Into<String>,
1352) -> AutonomousPrLaneDecision {
1353 let task_id: String = task_id.into();
1354 let detail: String = detail.into();
1355 let delivery_summary = format!("autonomous PR lane denied for task {task_id}: {detail}");
1356 AutonomousPrLaneDecision {
1357 pr_lane_id: pr_lane_id.into(),
1358 delivery_summary,
1359 branch_name: None,
1360 pr_payload: None,
1361 evidence_bundle: None,
1362 pr_ready: false,
1363 delivery_status: AutonomousPrLaneStatus::Denied,
1364 approval_state: PrLaneApprovalState::ClassNotApproved,
1365 reason_code,
1366 fail_closed: true,
1367 }
1368}
1369
1370#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1376#[serde(rename_all = "snake_case")]
1377pub enum ConfidenceState {
1378 Active,
1380 Decaying,
1383 Revalidating,
1386 Demoted,
1389 Quarantined,
1392}
1393
1394#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1396#[serde(rename_all = "snake_case")]
1397pub enum RevalidationOutcome {
1398 Passed,
1400 Failed,
1402 Pending,
1404 ErrorFailClosed,
1406}
1407
1408#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1410#[serde(rename_all = "snake_case")]
1411pub enum ConfidenceDemotionReasonCode {
1412 ConfidenceDecayThreshold,
1414 RepeatedReplayFailure,
1416 MaxFailureCountExceeded,
1418 ExplicitRevocation,
1420 UnknownFailClosed,
1422}
1423
1424#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1426#[serde(rename_all = "snake_case")]
1427pub enum ReplayEligibility {
1428 Eligible,
1430 Ineligible,
1432}
1433
1434#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1436pub struct ConfidenceRevalidationResult {
1437 pub revalidation_id: String,
1439 pub asset_id: String,
1441 pub confidence_state: ConfidenceState,
1443 pub revalidation_result: RevalidationOutcome,
1445 pub replay_eligibility: ReplayEligibility,
1447 pub summary: String,
1449 pub fail_closed: bool,
1452}
1453
1454#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1456pub struct DemotionDecision {
1457 pub demotion_id: String,
1459 pub asset_id: String,
1461 pub prior_state: ConfidenceState,
1463 pub new_state: ConfidenceState,
1465 pub reason_code: ConfidenceDemotionReasonCode,
1467 pub replay_eligibility: ReplayEligibility,
1469 pub summary: String,
1471 pub quarantine_transition: bool,
1473 pub fail_closed: bool,
1475}
1476
1477pub fn pass_confidence_revalidation(
1479 revalidation_id: impl Into<String>,
1480 asset_id: impl Into<String>,
1481 _prior_state: ConfidenceState,
1482) -> ConfidenceRevalidationResult {
1483 let asset_id: String = asset_id.into();
1484 let summary =
1485 format!("confidence revalidation passed for asset {asset_id}: restoring to Active");
1486 ConfidenceRevalidationResult {
1487 revalidation_id: revalidation_id.into(),
1488 asset_id,
1489 confidence_state: ConfidenceState::Active,
1490 revalidation_result: RevalidationOutcome::Passed,
1491 replay_eligibility: ReplayEligibility::Eligible,
1492 summary,
1493 fail_closed: false,
1494 }
1495}
1496
1497pub fn fail_confidence_revalidation(
1499 revalidation_id: impl Into<String>,
1500 asset_id: impl Into<String>,
1501 prior_state: ConfidenceState,
1502 outcome: RevalidationOutcome,
1503) -> ConfidenceRevalidationResult {
1504 let asset_id: String = asset_id.into();
1505 let summary = format!(
1506 "confidence revalidation failed for asset {asset_id} [{outcome:?}]: replay suspended"
1507 );
1508 ConfidenceRevalidationResult {
1509 revalidation_id: revalidation_id.into(),
1510 asset_id,
1511 confidence_state: prior_state,
1512 revalidation_result: outcome,
1513 replay_eligibility: ReplayEligibility::Ineligible,
1514 summary,
1515 fail_closed: true,
1516 }
1517}
1518
1519pub fn demote_asset(
1521 demotion_id: impl Into<String>,
1522 asset_id: impl Into<String>,
1523 prior_state: ConfidenceState,
1524 new_state: ConfidenceState,
1525 reason_code: ConfidenceDemotionReasonCode,
1526) -> DemotionDecision {
1527 let asset_id: String = asset_id.into();
1528 let quarantine_transition = new_state == ConfidenceState::Quarantined;
1529 let summary =
1530 format!("asset {asset_id} demoted from {prior_state:?} to {new_state:?} [{reason_code:?}]");
1531 DemotionDecision {
1532 demotion_id: demotion_id.into(),
1533 asset_id,
1534 prior_state,
1535 new_state,
1536 reason_code,
1537 replay_eligibility: ReplayEligibility::Ineligible,
1538 summary,
1539 quarantine_transition,
1540 fail_closed: true,
1541 }
1542}
1543
1544#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1545#[serde(rename_all = "snake_case")]
1546pub enum SelfEvolutionSelectionReasonCode {
1547 Accepted,
1548 IssueClosed,
1549 MissingEvolutionLabel,
1550 MissingFeatureLabel,
1551 ExcludedByLabel,
1552 UnsupportedCandidateScope,
1553 UnknownFailClosed,
1554}
1555
1556#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1557pub struct SelfEvolutionSelectionDecision {
1558 pub issue_number: u64,
1559 pub selected: bool,
1560 #[serde(default, skip_serializing_if = "Option::is_none")]
1561 pub candidate_class: Option<BoundedTaskClass>,
1562 pub summary: String,
1563 #[serde(default, skip_serializing_if = "Option::is_none")]
1564 pub reason_code: Option<SelfEvolutionSelectionReasonCode>,
1565 #[serde(default, skip_serializing_if = "Option::is_none")]
1566 pub failure_reason: Option<String>,
1567 #[serde(default, skip_serializing_if = "Option::is_none")]
1568 pub recovery_hint: Option<String>,
1569 pub fail_closed: bool,
1570}
1571
1572#[derive(Clone, Copy)]
1573struct SelfEvolutionSelectionDefaults {
1574 failure_reason: &'static str,
1575 recovery_hint: &'static str,
1576}
1577
1578fn self_evolution_selection_defaults(
1579 reason_code: &SelfEvolutionSelectionReasonCode,
1580) -> Option<SelfEvolutionSelectionDefaults> {
1581 match reason_code {
1582 SelfEvolutionSelectionReasonCode::Accepted => None,
1583 SelfEvolutionSelectionReasonCode::IssueClosed => Some(SelfEvolutionSelectionDefaults {
1584 failure_reason: "self-evolution candidate rejected because the issue is closed",
1585 recovery_hint: "Reopen the issue or choose an active open issue before retrying selection.",
1586 }),
1587 SelfEvolutionSelectionReasonCode::MissingEvolutionLabel => {
1588 Some(SelfEvolutionSelectionDefaults {
1589 failure_reason: "self-evolution candidate rejected because the issue is missing area/evolution",
1590 recovery_hint:
1591 "Add the area/evolution label or choose an issue already scoped to self-evolution.",
1592 })
1593 }
1594 SelfEvolutionSelectionReasonCode::MissingFeatureLabel => {
1595 Some(SelfEvolutionSelectionDefaults {
1596 failure_reason: "self-evolution candidate rejected because the issue is missing type/feature",
1597 recovery_hint:
1598 "Add the type/feature label or narrow the issue to a bounded feature slice before retrying.",
1599 })
1600 }
1601 SelfEvolutionSelectionReasonCode::ExcludedByLabel => Some(SelfEvolutionSelectionDefaults {
1602 failure_reason: "self-evolution candidate rejected by an excluded issue label",
1603 recovery_hint:
1604 "Remove the excluded label or choose a non-duplicate, non-invalid, actionable issue.",
1605 }),
1606 SelfEvolutionSelectionReasonCode::UnsupportedCandidateScope => {
1607 Some(SelfEvolutionSelectionDefaults {
1608 failure_reason:
1609 "self-evolution candidate rejected because the hinted file scope is outside the bounded docs policy",
1610 recovery_hint:
1611 "Narrow candidate paths to the approved docs/*.md boundary before retrying selection.",
1612 })
1613 }
1614 SelfEvolutionSelectionReasonCode::UnknownFailClosed => Some(SelfEvolutionSelectionDefaults {
1615 failure_reason: "self-evolution candidate failed with an unmapped selection reason",
1616 recovery_hint: "Unknown selection failure; fail closed and require explicit maintainer triage before retry.",
1617 }),
1618 }
1619}
1620
1621pub fn accept_self_evolution_selection_decision(
1622 issue_number: u64,
1623 candidate_class: BoundedTaskClass,
1624 summary: Option<&str>,
1625) -> SelfEvolutionSelectionDecision {
1626 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
1627 format!("selected GitHub issue #{issue_number} as a bounded self-evolution candidate")
1628 });
1629 SelfEvolutionSelectionDecision {
1630 issue_number,
1631 selected: true,
1632 candidate_class: Some(candidate_class),
1633 summary,
1634 reason_code: Some(SelfEvolutionSelectionReasonCode::Accepted),
1635 failure_reason: None,
1636 recovery_hint: None,
1637 fail_closed: false,
1638 }
1639}
1640
1641pub fn reject_self_evolution_selection_decision(
1642 issue_number: u64,
1643 reason_code: SelfEvolutionSelectionReasonCode,
1644 failure_reason: Option<&str>,
1645 summary: Option<&str>,
1646) -> SelfEvolutionSelectionDecision {
1647 let defaults = self_evolution_selection_defaults(&reason_code)
1648 .unwrap_or(SelfEvolutionSelectionDefaults {
1649 failure_reason: "self-evolution candidate rejected",
1650 recovery_hint:
1651 "Review candidate selection inputs and retry within the bounded self-evolution policy.",
1652 });
1653 let failure_reason = normalize_optional_text(failure_reason)
1654 .unwrap_or_else(|| defaults.failure_reason.to_string());
1655 let reason_code_key = match reason_code {
1656 SelfEvolutionSelectionReasonCode::Accepted => "accepted",
1657 SelfEvolutionSelectionReasonCode::IssueClosed => "issue_closed",
1658 SelfEvolutionSelectionReasonCode::MissingEvolutionLabel => "missing_evolution_label",
1659 SelfEvolutionSelectionReasonCode::MissingFeatureLabel => "missing_feature_label",
1660 SelfEvolutionSelectionReasonCode::ExcludedByLabel => "excluded_by_label",
1661 SelfEvolutionSelectionReasonCode::UnsupportedCandidateScope => {
1662 "unsupported_candidate_scope"
1663 }
1664 SelfEvolutionSelectionReasonCode::UnknownFailClosed => "unknown_fail_closed",
1665 };
1666 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
1667 format!(
1668 "rejected GitHub issue #{issue_number} as a self-evolution candidate [{reason_code_key}]"
1669 )
1670 });
1671
1672 SelfEvolutionSelectionDecision {
1673 issue_number,
1674 selected: false,
1675 candidate_class: None,
1676 summary,
1677 reason_code: Some(reason_code),
1678 failure_reason: Some(failure_reason),
1679 recovery_hint: Some(defaults.recovery_hint.to_string()),
1680 fail_closed: true,
1681 }
1682}
1683
1684pub fn accept_discovered_candidate(
1685 dedupe_key: impl Into<String>,
1686 candidate_source: AutonomousCandidateSource,
1687 candidate_class: BoundedTaskClass,
1688 signals: Vec<String>,
1689 summary: Option<&str>,
1690) -> DiscoveredCandidate {
1691 let summary = normalize_optional_text(summary)
1692 .unwrap_or_else(|| format!("accepted autonomous candidate from {candidate_source:?}"));
1693 DiscoveredCandidate {
1694 dedupe_key: dedupe_key.into(),
1695 candidate_source,
1696 candidate_class: Some(candidate_class),
1697 signals,
1698 accepted: true,
1699 reason_code: AutonomousIntakeReasonCode::Accepted,
1700 summary,
1701 failure_reason: None,
1702 recovery_hint: None,
1703 fail_closed: false,
1704 }
1705}
1706
1707pub fn deny_discovered_candidate(
1708 dedupe_key: impl Into<String>,
1709 candidate_source: AutonomousCandidateSource,
1710 signals: Vec<String>,
1711 reason_code: AutonomousIntakeReasonCode,
1712) -> DiscoveredCandidate {
1713 let (failure_reason, recovery_hint) = match reason_code {
1714 AutonomousIntakeReasonCode::UnsupportedSignalClass => (
1715 "signal class is not supported by the bounded evolution policy",
1716 "review supported candidate signal classes and filter input before retry",
1717 ),
1718 AutonomousIntakeReasonCode::AmbiguousSignal => (
1719 "signals do not map to a unique bounded candidate class",
1720 "provide more specific signal tokens or triage manually before resubmitting",
1721 ),
1722 AutonomousIntakeReasonCode::DuplicateCandidate => (
1723 "an equivalent candidate has already been discovered in this intake window",
1724 "deduplicate signals before resubmitting or check the existing candidate queue",
1725 ),
1726 AutonomousIntakeReasonCode::UnknownFailClosed => (
1727 "candidate intake failed with an unmapped reason; fail closed",
1728 "require explicit maintainer triage before retry",
1729 ),
1730 AutonomousIntakeReasonCode::Accepted => (
1731 "unexpected accepted reason on deny path",
1732 "use accept_discovered_candidate for accepted outcomes",
1733 ),
1734 };
1735 let summary =
1736 format!("denied autonomous candidate from {candidate_source:?}: {failure_reason}");
1737 DiscoveredCandidate {
1738 dedupe_key: dedupe_key.into(),
1739 candidate_source,
1740 candidate_class: None,
1741 signals,
1742 accepted: false,
1743 reason_code,
1744 summary,
1745 failure_reason: Some(failure_reason.to_string()),
1746 recovery_hint: Some(recovery_hint.to_string()),
1747 fail_closed: true,
1748 }
1749}
1750
1751#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1752pub struct HumanApproval {
1753 pub approved: bool,
1754 pub approver: Option<String>,
1755 pub note: Option<String>,
1756}
1757
1758#[derive(Clone, Debug, Serialize, Deserialize)]
1759pub struct SupervisedDevloopRequest {
1760 pub task: AgentTask,
1761 pub proposal: MutationProposal,
1762 pub approval: HumanApproval,
1763}
1764
1765#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1766pub enum SupervisedDevloopStatus {
1767 AwaitingApproval,
1768 RejectedByPolicy,
1769 FailedClosed,
1770 Executed,
1771}
1772
1773#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1774#[serde(rename_all = "snake_case")]
1775pub enum SupervisedDeliveryStatus {
1776 Prepared,
1777 Denied,
1778}
1779
1780#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1781#[serde(rename_all = "snake_case")]
1782pub enum SupervisedDeliveryApprovalState {
1783 Approved,
1784 MissingExplicitApproval,
1785}
1786
1787#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1788#[serde(rename_all = "snake_case")]
1789pub enum SupervisedDeliveryReasonCode {
1790 DeliveryPrepared,
1791 AwaitingApproval,
1792 DeliveryEvidenceMissing,
1793 ValidationEvidenceMissing,
1794 UnsupportedTaskScope,
1795 InconsistentDeliveryEvidence,
1796 UnknownFailClosed,
1797}
1798
1799#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1800pub struct SupervisedDeliveryContract {
1801 pub delivery_summary: String,
1802 #[serde(default, skip_serializing_if = "Option::is_none")]
1803 pub branch_name: Option<String>,
1804 #[serde(default, skip_serializing_if = "Option::is_none")]
1805 pub pr_title: Option<String>,
1806 #[serde(default, skip_serializing_if = "Option::is_none")]
1807 pub pr_summary: Option<String>,
1808 pub delivery_status: SupervisedDeliveryStatus,
1809 pub approval_state: SupervisedDeliveryApprovalState,
1810 pub reason_code: SupervisedDeliveryReasonCode,
1811 #[serde(default)]
1812 pub fail_closed: bool,
1813 #[serde(default, skip_serializing_if = "Option::is_none")]
1814 pub recovery_hint: Option<String>,
1815}
1816
1817#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1818#[serde(rename_all = "snake_case")]
1819pub enum SupervisedExecutionDecision {
1820 AwaitingApproval,
1821 ReplayHit,
1822 PlannerFallback,
1823 RejectedByPolicy,
1824 FailedClosed,
1825}
1826
1827#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1828#[serde(rename_all = "snake_case")]
1829pub enum SupervisedValidationOutcome {
1830 NotRun,
1831 Passed,
1832 FailedClosed,
1833}
1834
1835#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1836#[serde(rename_all = "snake_case")]
1837pub enum SupervisedExecutionReasonCode {
1838 AwaitingHumanApproval,
1839 ReplayHit,
1840 ReplayFallback,
1841 PolicyDenied,
1842 ValidationFailed,
1843 UnsafePatch,
1844 Timeout,
1845 MutationPayloadMissing,
1846 UnknownFailClosed,
1847}
1848#[derive(Clone, Debug, Serialize, Deserialize)]
1849pub struct SupervisedDevloopOutcome {
1850 pub task_id: String,
1851 pub task_class: Option<BoundedTaskClass>,
1852 pub status: SupervisedDevloopStatus,
1853 pub execution_decision: SupervisedExecutionDecision,
1854 #[serde(default, skip_serializing_if = "Option::is_none")]
1855 pub replay_outcome: Option<ReplayFeedback>,
1856 #[serde(default, skip_serializing_if = "Option::is_none")]
1857 pub fallback_reason: Option<String>,
1858 pub validation_outcome: SupervisedValidationOutcome,
1859 pub evidence_summary: String,
1860 #[serde(default, skip_serializing_if = "Option::is_none")]
1861 pub reason_code: Option<SupervisedExecutionReasonCode>,
1862 #[serde(default, skip_serializing_if = "Option::is_none")]
1863 pub recovery_hint: Option<String>,
1864 pub execution_feedback: Option<ExecutionFeedback>,
1865 #[serde(default, skip_serializing_if = "Option::is_none")]
1866 pub failure_contract: Option<MutationNeededFailureContract>,
1867 pub summary: String,
1868}
1869
1870#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1871#[serde(rename_all = "snake_case")]
1872pub enum SelfEvolutionAuditConsistencyResult {
1873 Consistent,
1874 Inconsistent,
1875}
1876
1877#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1878#[serde(rename_all = "snake_case")]
1879pub enum SelfEvolutionAcceptanceGateReasonCode {
1880 Accepted,
1881 MissingSelectionEvidence,
1882 MissingProposalEvidence,
1883 MissingApprovalEvidence,
1884 MissingExecutionEvidence,
1885 MissingDeliveryEvidence,
1886 InconsistentReasonCodeMatrix,
1887 UnknownFailClosed,
1888}
1889
1890#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1891pub struct SelfEvolutionApprovalEvidence {
1892 pub approval_required: bool,
1893 pub approved: bool,
1894 #[serde(default, skip_serializing_if = "Option::is_none")]
1895 pub approver: Option<String>,
1896}
1897
1898#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1899pub struct SelfEvolutionDeliveryOutcome {
1900 pub delivery_status: SupervisedDeliveryStatus,
1901 pub approval_state: SupervisedDeliveryApprovalState,
1902 pub reason_code: SupervisedDeliveryReasonCode,
1903}
1904
1905#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1906pub struct SelfEvolutionReasonCodeMatrix {
1907 #[serde(default, skip_serializing_if = "Option::is_none")]
1908 pub selection_reason_code: Option<SelfEvolutionSelectionReasonCode>,
1909 pub proposal_reason_code: MutationProposalContractReasonCode,
1910 #[serde(default, skip_serializing_if = "Option::is_none")]
1911 pub execution_reason_code: Option<SupervisedExecutionReasonCode>,
1912 pub delivery_reason_code: SupervisedDeliveryReasonCode,
1913}
1914
1915#[derive(Clone, Debug, Serialize, Deserialize)]
1916pub struct SelfEvolutionAcceptanceGateInput {
1917 pub selection_decision: SelfEvolutionSelectionDecision,
1918 pub proposal_contract: SelfEvolutionMutationProposalContract,
1919 pub supervised_request: SupervisedDevloopRequest,
1920 pub execution_outcome: SupervisedDevloopOutcome,
1921 pub delivery_contract: SupervisedDeliveryContract,
1922}
1923
1924#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1925pub struct SelfEvolutionAcceptanceGateContract {
1926 pub acceptance_gate_summary: String,
1927 pub audit_consistency_result: SelfEvolutionAuditConsistencyResult,
1928 pub approval_evidence: SelfEvolutionApprovalEvidence,
1929 pub delivery_outcome: SelfEvolutionDeliveryOutcome,
1930 pub reason_code_matrix: SelfEvolutionReasonCodeMatrix,
1931 pub fail_closed: bool,
1932 pub reason_code: SelfEvolutionAcceptanceGateReasonCode,
1933 #[serde(default, skip_serializing_if = "Option::is_none")]
1934 pub recovery_hint: Option<String>,
1935}
1936
1937#[cfg(test)]
1938mod tests {
1939 use super::*;
1940
1941 fn handshake_request_with_versions(versions: &[&str]) -> A2aHandshakeRequest {
1942 A2aHandshakeRequest {
1943 agent_id: "agent-test".into(),
1944 role: AgentRole::Planner,
1945 capability_level: AgentCapabilityLevel::A2,
1946 supported_protocols: versions
1947 .iter()
1948 .map(|version| A2aProtocol {
1949 name: A2A_PROTOCOL_NAME.into(),
1950 version: (*version).into(),
1951 })
1952 .collect(),
1953 advertised_capabilities: vec![A2aCapability::Coordination],
1954 }
1955 }
1956
1957 #[test]
1958 fn negotiate_supported_protocol_prefers_v1_when_available() {
1959 let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION, A2A_PROTOCOL_VERSION_V1]);
1960 let negotiated = req
1961 .negotiate_supported_protocol()
1962 .expect("expected protocol negotiation success");
1963 assert_eq!(negotiated.name, A2A_PROTOCOL_NAME);
1964 assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION_V1);
1965 }
1966
1967 #[test]
1968 fn negotiate_supported_protocol_falls_back_to_experimental() {
1969 let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION]);
1970 let negotiated = req
1971 .negotiate_supported_protocol()
1972 .expect("expected protocol negotiation success");
1973 assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION);
1974 }
1975
1976 #[test]
1977 fn negotiate_supported_protocol_returns_none_without_overlap() {
1978 let req = handshake_request_with_versions(&["0.0.1"]);
1979 assert!(req.negotiate_supported_protocol().is_none());
1980 }
1981
1982 #[test]
1983 fn normalize_replay_fallback_contract_maps_known_reason() {
1984 let contract = normalize_replay_fallback_contract(
1985 &ReplayPlannerDirective::PlanFallback,
1986 Some("no matching gene"),
1987 None,
1988 None,
1989 None,
1990 None,
1991 )
1992 .expect("contract should exist");
1993
1994 assert_eq!(
1995 contract.reason_code,
1996 ReplayFallbackReasonCode::NoCandidateAfterSelect
1997 );
1998 assert_eq!(
1999 contract.next_action,
2000 ReplayFallbackNextAction::PlanFromScratch
2001 );
2002 assert_eq!(contract.confidence, 92);
2003 }
2004
2005 #[test]
2006 fn normalize_replay_fallback_contract_fails_closed_for_unknown_reason() {
2007 let contract = normalize_replay_fallback_contract(
2008 &ReplayPlannerDirective::PlanFallback,
2009 Some("something unexpected"),
2010 None,
2011 None,
2012 None,
2013 None,
2014 )
2015 .expect("contract should exist");
2016
2017 assert_eq!(
2018 contract.reason_code,
2019 ReplayFallbackReasonCode::UnmappedFallbackReason
2020 );
2021 assert_eq!(
2022 contract.next_action,
2023 ReplayFallbackNextAction::EscalateFailClosed
2024 );
2025 assert_eq!(contract.confidence, 0);
2026 }
2027
2028 #[test]
2029 fn normalize_replay_fallback_contract_rejects_conflicting_next_action() {
2030 let contract = normalize_replay_fallback_contract(
2031 &ReplayPlannerDirective::PlanFallback,
2032 Some("replay validation failed"),
2033 Some(ReplayFallbackReasonCode::ValidationFailed),
2034 None,
2035 Some(ReplayFallbackNextAction::PlanFromScratch),
2036 Some(88),
2037 )
2038 .expect("contract should exist");
2039
2040 assert_eq!(
2041 contract.reason_code,
2042 ReplayFallbackReasonCode::UnmappedFallbackReason
2043 );
2044 assert_eq!(
2045 contract.next_action,
2046 ReplayFallbackNextAction::EscalateFailClosed
2047 );
2048 assert_eq!(contract.confidence, 0);
2049 }
2050
2051 #[test]
2052 fn normalize_mutation_needed_failure_contract_maps_policy_denied() {
2053 let contract = normalize_mutation_needed_failure_contract(
2054 Some("supervised devloop rejected task because it is outside bounded scope"),
2055 None,
2056 );
2057
2058 assert_eq!(
2059 contract.reason_code,
2060 MutationNeededFailureReasonCode::PolicyDenied
2061 );
2062 assert_eq!(
2063 contract.recovery_action,
2064 MutationNeededRecoveryAction::NarrowScopeAndRetry
2065 );
2066 assert!(contract.fail_closed);
2067 }
2068
2069 #[test]
2070 fn normalize_mutation_needed_failure_contract_maps_timeout() {
2071 let contract = normalize_mutation_needed_failure_contract(
2072 Some("command timed out: git apply --check patch.diff"),
2073 None,
2074 );
2075
2076 assert_eq!(
2077 contract.reason_code,
2078 MutationNeededFailureReasonCode::Timeout
2079 );
2080 assert_eq!(
2081 contract.recovery_action,
2082 MutationNeededRecoveryAction::ReduceExecutionBudget
2083 );
2084 assert!(contract.fail_closed);
2085 }
2086
2087 #[test]
2088 fn normalize_mutation_needed_failure_contract_fails_closed_for_unknown_reason() {
2089 let contract =
2090 normalize_mutation_needed_failure_contract(Some("unexpected runner panic"), None);
2091
2092 assert_eq!(
2093 contract.reason_code,
2094 MutationNeededFailureReasonCode::UnknownFailClosed
2095 );
2096 assert_eq!(
2097 contract.recovery_action,
2098 MutationNeededRecoveryAction::EscalateFailClosed
2099 );
2100 assert!(contract.fail_closed);
2101 }
2102
2103 #[test]
2104 fn reject_self_evolution_selection_decision_maps_closed_issue_defaults() {
2105 let decision = reject_self_evolution_selection_decision(
2106 234,
2107 SelfEvolutionSelectionReasonCode::IssueClosed,
2108 None,
2109 None,
2110 );
2111
2112 assert!(!decision.selected);
2113 assert_eq!(decision.issue_number, 234);
2114 assert_eq!(
2115 decision.reason_code,
2116 Some(SelfEvolutionSelectionReasonCode::IssueClosed)
2117 );
2118 assert!(decision.fail_closed);
2119 assert!(decision
2120 .failure_reason
2121 .as_deref()
2122 .is_some_and(|reason| reason.contains("closed")));
2123 assert!(decision.recovery_hint.is_some());
2124 }
2125
2126 #[test]
2127 fn accept_self_evolution_selection_decision_marks_candidate_selected() {
2128 let decision =
2129 accept_self_evolution_selection_decision(235, BoundedTaskClass::DocsSingleFile, None);
2130
2131 assert!(decision.selected);
2132 assert_eq!(decision.issue_number, 235);
2133 assert_eq!(
2134 decision.candidate_class,
2135 Some(BoundedTaskClass::DocsSingleFile)
2136 );
2137 assert_eq!(
2138 decision.reason_code,
2139 Some(SelfEvolutionSelectionReasonCode::Accepted)
2140 );
2141 assert!(!decision.fail_closed);
2142 assert_eq!(decision.failure_reason, None);
2143 assert_eq!(decision.recovery_hint, None);
2144 }
2145}
2146
2147#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
2149pub enum HubTrustTier {
2150 Full,
2152 ReadOnly,
2154}
2155
2156#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
2158pub enum HubOperationClass {
2159 Hello,
2160 Fetch,
2161 Publish,
2162 Revoke,
2163 TaskClaim,
2164 TaskComplete,
2165 WorkerRegister,
2166 Recipe,
2167 Session,
2168 Dispute,
2169 Swarm,
2170}
2171
2172impl HubOperationClass {
2173 pub fn is_read_only(&self) -> bool {
2175 matches!(self, HubOperationClass::Hello | HubOperationClass::Fetch)
2176 }
2177}
2178
2179#[derive(Clone, Debug, Serialize, Deserialize)]
2181pub struct HubProfile {
2182 pub hub_id: String,
2183 pub base_url: String,
2184 pub trust_tier: HubTrustTier,
2185 pub priority: u32,
2187 pub health_url: Option<String>,
2189}
2190
2191impl HubProfile {
2192 pub fn allows_operation(&self, operation: &HubOperationClass) -> bool {
2194 match &self.trust_tier {
2195 HubTrustTier::Full => true,
2196 HubTrustTier::ReadOnly => operation.is_read_only(),
2197 }
2198 }
2199}
2200
2201#[derive(Clone, Debug)]
2203pub struct HubSelectionPolicy {
2204 pub allowed_tiers_for_operation: Vec<(HubOperationClass, Vec<HubTrustTier>)>,
2206 pub default_allowed_tiers: Vec<HubTrustTier>,
2208}
2209
2210impl Default for HubSelectionPolicy {
2211 fn default() -> Self {
2212 Self {
2213 allowed_tiers_for_operation: vec![
2214 (
2215 HubOperationClass::Hello,
2216 vec![HubTrustTier::Full, HubTrustTier::ReadOnly],
2217 ),
2218 (
2219 HubOperationClass::Fetch,
2220 vec![HubTrustTier::Full, HubTrustTier::ReadOnly],
2221 ),
2222 (HubOperationClass::Publish, vec![HubTrustTier::Full]),
2224 (HubOperationClass::Revoke, vec![HubTrustTier::Full]),
2225 (HubOperationClass::TaskClaim, vec![HubTrustTier::Full]),
2226 (HubOperationClass::TaskComplete, vec![HubTrustTier::Full]),
2227 (HubOperationClass::WorkerRegister, vec![HubTrustTier::Full]),
2228 (HubOperationClass::Recipe, vec![HubTrustTier::Full]),
2229 (HubOperationClass::Session, vec![HubTrustTier::Full]),
2230 (HubOperationClass::Dispute, vec![HubTrustTier::Full]),
2231 (HubOperationClass::Swarm, vec![HubTrustTier::Full]),
2232 ],
2233 default_allowed_tiers: vec![HubTrustTier::Full],
2234 }
2235 }
2236}
2237
2238impl HubSelectionPolicy {
2239 pub fn allowed_tiers(&self, operation: &HubOperationClass) -> &[HubTrustTier] {
2241 self.allowed_tiers_for_operation
2242 .iter()
2243 .find(|(op, _)| op == operation)
2244 .map(|(_, tiers)| tiers.as_slice())
2245 .unwrap_or(&self.default_allowed_tiers)
2246 }
2247}