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)]
1238#[serde(rename_all = "snake_case")]
1239pub enum SelfEvolutionSelectionReasonCode {
1240 Accepted,
1241 IssueClosed,
1242 MissingEvolutionLabel,
1243 MissingFeatureLabel,
1244 ExcludedByLabel,
1245 UnsupportedCandidateScope,
1246 UnknownFailClosed,
1247}
1248
1249#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1250pub struct SelfEvolutionSelectionDecision {
1251 pub issue_number: u64,
1252 pub selected: bool,
1253 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub candidate_class: Option<BoundedTaskClass>,
1255 pub summary: String,
1256 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub reason_code: Option<SelfEvolutionSelectionReasonCode>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1259 pub failure_reason: Option<String>,
1260 #[serde(default, skip_serializing_if = "Option::is_none")]
1261 pub recovery_hint: Option<String>,
1262 pub fail_closed: bool,
1263}
1264
1265#[derive(Clone, Copy)]
1266struct SelfEvolutionSelectionDefaults {
1267 failure_reason: &'static str,
1268 recovery_hint: &'static str,
1269}
1270
1271fn self_evolution_selection_defaults(
1272 reason_code: &SelfEvolutionSelectionReasonCode,
1273) -> Option<SelfEvolutionSelectionDefaults> {
1274 match reason_code {
1275 SelfEvolutionSelectionReasonCode::Accepted => None,
1276 SelfEvolutionSelectionReasonCode::IssueClosed => Some(SelfEvolutionSelectionDefaults {
1277 failure_reason: "self-evolution candidate rejected because the issue is closed",
1278 recovery_hint: "Reopen the issue or choose an active open issue before retrying selection.",
1279 }),
1280 SelfEvolutionSelectionReasonCode::MissingEvolutionLabel => {
1281 Some(SelfEvolutionSelectionDefaults {
1282 failure_reason: "self-evolution candidate rejected because the issue is missing area/evolution",
1283 recovery_hint:
1284 "Add the area/evolution label or choose an issue already scoped to self-evolution.",
1285 })
1286 }
1287 SelfEvolutionSelectionReasonCode::MissingFeatureLabel => {
1288 Some(SelfEvolutionSelectionDefaults {
1289 failure_reason: "self-evolution candidate rejected because the issue is missing type/feature",
1290 recovery_hint:
1291 "Add the type/feature label or narrow the issue to a bounded feature slice before retrying.",
1292 })
1293 }
1294 SelfEvolutionSelectionReasonCode::ExcludedByLabel => Some(SelfEvolutionSelectionDefaults {
1295 failure_reason: "self-evolution candidate rejected by an excluded issue label",
1296 recovery_hint:
1297 "Remove the excluded label or choose a non-duplicate, non-invalid, actionable issue.",
1298 }),
1299 SelfEvolutionSelectionReasonCode::UnsupportedCandidateScope => {
1300 Some(SelfEvolutionSelectionDefaults {
1301 failure_reason:
1302 "self-evolution candidate rejected because the hinted file scope is outside the bounded docs policy",
1303 recovery_hint:
1304 "Narrow candidate paths to the approved docs/*.md boundary before retrying selection.",
1305 })
1306 }
1307 SelfEvolutionSelectionReasonCode::UnknownFailClosed => Some(SelfEvolutionSelectionDefaults {
1308 failure_reason: "self-evolution candidate failed with an unmapped selection reason",
1309 recovery_hint: "Unknown selection failure; fail closed and require explicit maintainer triage before retry.",
1310 }),
1311 }
1312}
1313
1314pub fn accept_self_evolution_selection_decision(
1315 issue_number: u64,
1316 candidate_class: BoundedTaskClass,
1317 summary: Option<&str>,
1318) -> SelfEvolutionSelectionDecision {
1319 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
1320 format!("selected GitHub issue #{issue_number} as a bounded self-evolution candidate")
1321 });
1322 SelfEvolutionSelectionDecision {
1323 issue_number,
1324 selected: true,
1325 candidate_class: Some(candidate_class),
1326 summary,
1327 reason_code: Some(SelfEvolutionSelectionReasonCode::Accepted),
1328 failure_reason: None,
1329 recovery_hint: None,
1330 fail_closed: false,
1331 }
1332}
1333
1334pub fn reject_self_evolution_selection_decision(
1335 issue_number: u64,
1336 reason_code: SelfEvolutionSelectionReasonCode,
1337 failure_reason: Option<&str>,
1338 summary: Option<&str>,
1339) -> SelfEvolutionSelectionDecision {
1340 let defaults = self_evolution_selection_defaults(&reason_code)
1341 .unwrap_or(SelfEvolutionSelectionDefaults {
1342 failure_reason: "self-evolution candidate rejected",
1343 recovery_hint:
1344 "Review candidate selection inputs and retry within the bounded self-evolution policy.",
1345 });
1346 let failure_reason = normalize_optional_text(failure_reason)
1347 .unwrap_or_else(|| defaults.failure_reason.to_string());
1348 let reason_code_key = match reason_code {
1349 SelfEvolutionSelectionReasonCode::Accepted => "accepted",
1350 SelfEvolutionSelectionReasonCode::IssueClosed => "issue_closed",
1351 SelfEvolutionSelectionReasonCode::MissingEvolutionLabel => "missing_evolution_label",
1352 SelfEvolutionSelectionReasonCode::MissingFeatureLabel => "missing_feature_label",
1353 SelfEvolutionSelectionReasonCode::ExcludedByLabel => "excluded_by_label",
1354 SelfEvolutionSelectionReasonCode::UnsupportedCandidateScope => {
1355 "unsupported_candidate_scope"
1356 }
1357 SelfEvolutionSelectionReasonCode::UnknownFailClosed => "unknown_fail_closed",
1358 };
1359 let summary = normalize_optional_text(summary).unwrap_or_else(|| {
1360 format!(
1361 "rejected GitHub issue #{issue_number} as a self-evolution candidate [{reason_code_key}]"
1362 )
1363 });
1364
1365 SelfEvolutionSelectionDecision {
1366 issue_number,
1367 selected: false,
1368 candidate_class: None,
1369 summary,
1370 reason_code: Some(reason_code),
1371 failure_reason: Some(failure_reason),
1372 recovery_hint: Some(defaults.recovery_hint.to_string()),
1373 fail_closed: true,
1374 }
1375}
1376
1377pub fn accept_discovered_candidate(
1378 dedupe_key: impl Into<String>,
1379 candidate_source: AutonomousCandidateSource,
1380 candidate_class: BoundedTaskClass,
1381 signals: Vec<String>,
1382 summary: Option<&str>,
1383) -> DiscoveredCandidate {
1384 let summary = normalize_optional_text(summary)
1385 .unwrap_or_else(|| format!("accepted autonomous candidate from {candidate_source:?}"));
1386 DiscoveredCandidate {
1387 dedupe_key: dedupe_key.into(),
1388 candidate_source,
1389 candidate_class: Some(candidate_class),
1390 signals,
1391 accepted: true,
1392 reason_code: AutonomousIntakeReasonCode::Accepted,
1393 summary,
1394 failure_reason: None,
1395 recovery_hint: None,
1396 fail_closed: false,
1397 }
1398}
1399
1400pub fn deny_discovered_candidate(
1401 dedupe_key: impl Into<String>,
1402 candidate_source: AutonomousCandidateSource,
1403 signals: Vec<String>,
1404 reason_code: AutonomousIntakeReasonCode,
1405) -> DiscoveredCandidate {
1406 let (failure_reason, recovery_hint) = match reason_code {
1407 AutonomousIntakeReasonCode::UnsupportedSignalClass => (
1408 "signal class is not supported by the bounded evolution policy",
1409 "review supported candidate signal classes and filter input before retry",
1410 ),
1411 AutonomousIntakeReasonCode::AmbiguousSignal => (
1412 "signals do not map to a unique bounded candidate class",
1413 "provide more specific signal tokens or triage manually before resubmitting",
1414 ),
1415 AutonomousIntakeReasonCode::DuplicateCandidate => (
1416 "an equivalent candidate has already been discovered in this intake window",
1417 "deduplicate signals before resubmitting or check the existing candidate queue",
1418 ),
1419 AutonomousIntakeReasonCode::UnknownFailClosed => (
1420 "candidate intake failed with an unmapped reason; fail closed",
1421 "require explicit maintainer triage before retry",
1422 ),
1423 AutonomousIntakeReasonCode::Accepted => (
1424 "unexpected accepted reason on deny path",
1425 "use accept_discovered_candidate for accepted outcomes",
1426 ),
1427 };
1428 let summary =
1429 format!("denied autonomous candidate from {candidate_source:?}: {failure_reason}");
1430 DiscoveredCandidate {
1431 dedupe_key: dedupe_key.into(),
1432 candidate_source,
1433 candidate_class: None,
1434 signals,
1435 accepted: false,
1436 reason_code,
1437 summary,
1438 failure_reason: Some(failure_reason.to_string()),
1439 recovery_hint: Some(recovery_hint.to_string()),
1440 fail_closed: true,
1441 }
1442}
1443
1444#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1445pub struct HumanApproval {
1446 pub approved: bool,
1447 pub approver: Option<String>,
1448 pub note: Option<String>,
1449}
1450
1451#[derive(Clone, Debug, Serialize, Deserialize)]
1452pub struct SupervisedDevloopRequest {
1453 pub task: AgentTask,
1454 pub proposal: MutationProposal,
1455 pub approval: HumanApproval,
1456}
1457
1458#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1459pub enum SupervisedDevloopStatus {
1460 AwaitingApproval,
1461 RejectedByPolicy,
1462 FailedClosed,
1463 Executed,
1464}
1465
1466#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1467#[serde(rename_all = "snake_case")]
1468pub enum SupervisedDeliveryStatus {
1469 Prepared,
1470 Denied,
1471}
1472
1473#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1474#[serde(rename_all = "snake_case")]
1475pub enum SupervisedDeliveryApprovalState {
1476 Approved,
1477 MissingExplicitApproval,
1478}
1479
1480#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1481#[serde(rename_all = "snake_case")]
1482pub enum SupervisedDeliveryReasonCode {
1483 DeliveryPrepared,
1484 AwaitingApproval,
1485 DeliveryEvidenceMissing,
1486 ValidationEvidenceMissing,
1487 UnsupportedTaskScope,
1488 InconsistentDeliveryEvidence,
1489 UnknownFailClosed,
1490}
1491
1492#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1493pub struct SupervisedDeliveryContract {
1494 pub delivery_summary: String,
1495 #[serde(default, skip_serializing_if = "Option::is_none")]
1496 pub branch_name: Option<String>,
1497 #[serde(default, skip_serializing_if = "Option::is_none")]
1498 pub pr_title: Option<String>,
1499 #[serde(default, skip_serializing_if = "Option::is_none")]
1500 pub pr_summary: Option<String>,
1501 pub delivery_status: SupervisedDeliveryStatus,
1502 pub approval_state: SupervisedDeliveryApprovalState,
1503 pub reason_code: SupervisedDeliveryReasonCode,
1504 #[serde(default)]
1505 pub fail_closed: bool,
1506 #[serde(default, skip_serializing_if = "Option::is_none")]
1507 pub recovery_hint: Option<String>,
1508}
1509
1510#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1511#[serde(rename_all = "snake_case")]
1512pub enum SupervisedExecutionDecision {
1513 AwaitingApproval,
1514 ReplayHit,
1515 PlannerFallback,
1516 RejectedByPolicy,
1517 FailedClosed,
1518}
1519
1520#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1521#[serde(rename_all = "snake_case")]
1522pub enum SupervisedValidationOutcome {
1523 NotRun,
1524 Passed,
1525 FailedClosed,
1526}
1527
1528#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1529#[serde(rename_all = "snake_case")]
1530pub enum SupervisedExecutionReasonCode {
1531 AwaitingHumanApproval,
1532 ReplayHit,
1533 ReplayFallback,
1534 PolicyDenied,
1535 ValidationFailed,
1536 UnsafePatch,
1537 Timeout,
1538 MutationPayloadMissing,
1539 UnknownFailClosed,
1540}
1541#[derive(Clone, Debug, Serialize, Deserialize)]
1542pub struct SupervisedDevloopOutcome {
1543 pub task_id: String,
1544 pub task_class: Option<BoundedTaskClass>,
1545 pub status: SupervisedDevloopStatus,
1546 pub execution_decision: SupervisedExecutionDecision,
1547 #[serde(default, skip_serializing_if = "Option::is_none")]
1548 pub replay_outcome: Option<ReplayFeedback>,
1549 #[serde(default, skip_serializing_if = "Option::is_none")]
1550 pub fallback_reason: Option<String>,
1551 pub validation_outcome: SupervisedValidationOutcome,
1552 pub evidence_summary: String,
1553 #[serde(default, skip_serializing_if = "Option::is_none")]
1554 pub reason_code: Option<SupervisedExecutionReasonCode>,
1555 #[serde(default, skip_serializing_if = "Option::is_none")]
1556 pub recovery_hint: Option<String>,
1557 pub execution_feedback: Option<ExecutionFeedback>,
1558 #[serde(default, skip_serializing_if = "Option::is_none")]
1559 pub failure_contract: Option<MutationNeededFailureContract>,
1560 pub summary: String,
1561}
1562
1563#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1564#[serde(rename_all = "snake_case")]
1565pub enum SelfEvolutionAuditConsistencyResult {
1566 Consistent,
1567 Inconsistent,
1568}
1569
1570#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
1571#[serde(rename_all = "snake_case")]
1572pub enum SelfEvolutionAcceptanceGateReasonCode {
1573 Accepted,
1574 MissingSelectionEvidence,
1575 MissingProposalEvidence,
1576 MissingApprovalEvidence,
1577 MissingExecutionEvidence,
1578 MissingDeliveryEvidence,
1579 InconsistentReasonCodeMatrix,
1580 UnknownFailClosed,
1581}
1582
1583#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1584pub struct SelfEvolutionApprovalEvidence {
1585 pub approval_required: bool,
1586 pub approved: bool,
1587 #[serde(default, skip_serializing_if = "Option::is_none")]
1588 pub approver: Option<String>,
1589}
1590
1591#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1592pub struct SelfEvolutionDeliveryOutcome {
1593 pub delivery_status: SupervisedDeliveryStatus,
1594 pub approval_state: SupervisedDeliveryApprovalState,
1595 pub reason_code: SupervisedDeliveryReasonCode,
1596}
1597
1598#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1599pub struct SelfEvolutionReasonCodeMatrix {
1600 #[serde(default, skip_serializing_if = "Option::is_none")]
1601 pub selection_reason_code: Option<SelfEvolutionSelectionReasonCode>,
1602 pub proposal_reason_code: MutationProposalContractReasonCode,
1603 #[serde(default, skip_serializing_if = "Option::is_none")]
1604 pub execution_reason_code: Option<SupervisedExecutionReasonCode>,
1605 pub delivery_reason_code: SupervisedDeliveryReasonCode,
1606}
1607
1608#[derive(Clone, Debug, Serialize, Deserialize)]
1609pub struct SelfEvolutionAcceptanceGateInput {
1610 pub selection_decision: SelfEvolutionSelectionDecision,
1611 pub proposal_contract: SelfEvolutionMutationProposalContract,
1612 pub supervised_request: SupervisedDevloopRequest,
1613 pub execution_outcome: SupervisedDevloopOutcome,
1614 pub delivery_contract: SupervisedDeliveryContract,
1615}
1616
1617#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1618pub struct SelfEvolutionAcceptanceGateContract {
1619 pub acceptance_gate_summary: String,
1620 pub audit_consistency_result: SelfEvolutionAuditConsistencyResult,
1621 pub approval_evidence: SelfEvolutionApprovalEvidence,
1622 pub delivery_outcome: SelfEvolutionDeliveryOutcome,
1623 pub reason_code_matrix: SelfEvolutionReasonCodeMatrix,
1624 pub fail_closed: bool,
1625 pub reason_code: SelfEvolutionAcceptanceGateReasonCode,
1626 #[serde(default, skip_serializing_if = "Option::is_none")]
1627 pub recovery_hint: Option<String>,
1628}
1629
1630#[cfg(test)]
1631mod tests {
1632 use super::*;
1633
1634 fn handshake_request_with_versions(versions: &[&str]) -> A2aHandshakeRequest {
1635 A2aHandshakeRequest {
1636 agent_id: "agent-test".into(),
1637 role: AgentRole::Planner,
1638 capability_level: AgentCapabilityLevel::A2,
1639 supported_protocols: versions
1640 .iter()
1641 .map(|version| A2aProtocol {
1642 name: A2A_PROTOCOL_NAME.into(),
1643 version: (*version).into(),
1644 })
1645 .collect(),
1646 advertised_capabilities: vec![A2aCapability::Coordination],
1647 }
1648 }
1649
1650 #[test]
1651 fn negotiate_supported_protocol_prefers_v1_when_available() {
1652 let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION, A2A_PROTOCOL_VERSION_V1]);
1653 let negotiated = req
1654 .negotiate_supported_protocol()
1655 .expect("expected protocol negotiation success");
1656 assert_eq!(negotiated.name, A2A_PROTOCOL_NAME);
1657 assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION_V1);
1658 }
1659
1660 #[test]
1661 fn negotiate_supported_protocol_falls_back_to_experimental() {
1662 let req = handshake_request_with_versions(&[A2A_PROTOCOL_VERSION]);
1663 let negotiated = req
1664 .negotiate_supported_protocol()
1665 .expect("expected protocol negotiation success");
1666 assert_eq!(negotiated.version, A2A_PROTOCOL_VERSION);
1667 }
1668
1669 #[test]
1670 fn negotiate_supported_protocol_returns_none_without_overlap() {
1671 let req = handshake_request_with_versions(&["0.0.1"]);
1672 assert!(req.negotiate_supported_protocol().is_none());
1673 }
1674
1675 #[test]
1676 fn normalize_replay_fallback_contract_maps_known_reason() {
1677 let contract = normalize_replay_fallback_contract(
1678 &ReplayPlannerDirective::PlanFallback,
1679 Some("no matching gene"),
1680 None,
1681 None,
1682 None,
1683 None,
1684 )
1685 .expect("contract should exist");
1686
1687 assert_eq!(
1688 contract.reason_code,
1689 ReplayFallbackReasonCode::NoCandidateAfterSelect
1690 );
1691 assert_eq!(
1692 contract.next_action,
1693 ReplayFallbackNextAction::PlanFromScratch
1694 );
1695 assert_eq!(contract.confidence, 92);
1696 }
1697
1698 #[test]
1699 fn normalize_replay_fallback_contract_fails_closed_for_unknown_reason() {
1700 let contract = normalize_replay_fallback_contract(
1701 &ReplayPlannerDirective::PlanFallback,
1702 Some("something unexpected"),
1703 None,
1704 None,
1705 None,
1706 None,
1707 )
1708 .expect("contract should exist");
1709
1710 assert_eq!(
1711 contract.reason_code,
1712 ReplayFallbackReasonCode::UnmappedFallbackReason
1713 );
1714 assert_eq!(
1715 contract.next_action,
1716 ReplayFallbackNextAction::EscalateFailClosed
1717 );
1718 assert_eq!(contract.confidence, 0);
1719 }
1720
1721 #[test]
1722 fn normalize_replay_fallback_contract_rejects_conflicting_next_action() {
1723 let contract = normalize_replay_fallback_contract(
1724 &ReplayPlannerDirective::PlanFallback,
1725 Some("replay validation failed"),
1726 Some(ReplayFallbackReasonCode::ValidationFailed),
1727 None,
1728 Some(ReplayFallbackNextAction::PlanFromScratch),
1729 Some(88),
1730 )
1731 .expect("contract should exist");
1732
1733 assert_eq!(
1734 contract.reason_code,
1735 ReplayFallbackReasonCode::UnmappedFallbackReason
1736 );
1737 assert_eq!(
1738 contract.next_action,
1739 ReplayFallbackNextAction::EscalateFailClosed
1740 );
1741 assert_eq!(contract.confidence, 0);
1742 }
1743
1744 #[test]
1745 fn normalize_mutation_needed_failure_contract_maps_policy_denied() {
1746 let contract = normalize_mutation_needed_failure_contract(
1747 Some("supervised devloop rejected task because it is outside bounded scope"),
1748 None,
1749 );
1750
1751 assert_eq!(
1752 contract.reason_code,
1753 MutationNeededFailureReasonCode::PolicyDenied
1754 );
1755 assert_eq!(
1756 contract.recovery_action,
1757 MutationNeededRecoveryAction::NarrowScopeAndRetry
1758 );
1759 assert!(contract.fail_closed);
1760 }
1761
1762 #[test]
1763 fn normalize_mutation_needed_failure_contract_maps_timeout() {
1764 let contract = normalize_mutation_needed_failure_contract(
1765 Some("command timed out: git apply --check patch.diff"),
1766 None,
1767 );
1768
1769 assert_eq!(
1770 contract.reason_code,
1771 MutationNeededFailureReasonCode::Timeout
1772 );
1773 assert_eq!(
1774 contract.recovery_action,
1775 MutationNeededRecoveryAction::ReduceExecutionBudget
1776 );
1777 assert!(contract.fail_closed);
1778 }
1779
1780 #[test]
1781 fn normalize_mutation_needed_failure_contract_fails_closed_for_unknown_reason() {
1782 let contract =
1783 normalize_mutation_needed_failure_contract(Some("unexpected runner panic"), None);
1784
1785 assert_eq!(
1786 contract.reason_code,
1787 MutationNeededFailureReasonCode::UnknownFailClosed
1788 );
1789 assert_eq!(
1790 contract.recovery_action,
1791 MutationNeededRecoveryAction::EscalateFailClosed
1792 );
1793 assert!(contract.fail_closed);
1794 }
1795
1796 #[test]
1797 fn reject_self_evolution_selection_decision_maps_closed_issue_defaults() {
1798 let decision = reject_self_evolution_selection_decision(
1799 234,
1800 SelfEvolutionSelectionReasonCode::IssueClosed,
1801 None,
1802 None,
1803 );
1804
1805 assert!(!decision.selected);
1806 assert_eq!(decision.issue_number, 234);
1807 assert_eq!(
1808 decision.reason_code,
1809 Some(SelfEvolutionSelectionReasonCode::IssueClosed)
1810 );
1811 assert!(decision.fail_closed);
1812 assert!(decision
1813 .failure_reason
1814 .as_deref()
1815 .is_some_and(|reason| reason.contains("closed")));
1816 assert!(decision.recovery_hint.is_some());
1817 }
1818
1819 #[test]
1820 fn accept_self_evolution_selection_decision_marks_candidate_selected() {
1821 let decision =
1822 accept_self_evolution_selection_decision(235, BoundedTaskClass::DocsSingleFile, None);
1823
1824 assert!(decision.selected);
1825 assert_eq!(decision.issue_number, 235);
1826 assert_eq!(
1827 decision.candidate_class,
1828 Some(BoundedTaskClass::DocsSingleFile)
1829 );
1830 assert_eq!(
1831 decision.reason_code,
1832 Some(SelfEvolutionSelectionReasonCode::Accepted)
1833 );
1834 assert!(!decision.fail_closed);
1835 assert_eq!(decision.failure_reason, None);
1836 assert_eq!(decision.recovery_hint, None);
1837 }
1838}
1839
1840#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1842pub enum HubTrustTier {
1843 Full,
1845 ReadOnly,
1847}
1848
1849#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
1851pub enum HubOperationClass {
1852 Hello,
1853 Fetch,
1854 Publish,
1855 Revoke,
1856 TaskClaim,
1857 TaskComplete,
1858 WorkerRegister,
1859 Recipe,
1860 Session,
1861 Dispute,
1862 Swarm,
1863}
1864
1865impl HubOperationClass {
1866 pub fn is_read_only(&self) -> bool {
1868 matches!(self, HubOperationClass::Hello | HubOperationClass::Fetch)
1869 }
1870}
1871
1872#[derive(Clone, Debug, Serialize, Deserialize)]
1874pub struct HubProfile {
1875 pub hub_id: String,
1876 pub base_url: String,
1877 pub trust_tier: HubTrustTier,
1878 pub priority: u32,
1880 pub health_url: Option<String>,
1882}
1883
1884impl HubProfile {
1885 pub fn allows_operation(&self, operation: &HubOperationClass) -> bool {
1887 match &self.trust_tier {
1888 HubTrustTier::Full => true,
1889 HubTrustTier::ReadOnly => operation.is_read_only(),
1890 }
1891 }
1892}
1893
1894#[derive(Clone, Debug)]
1896pub struct HubSelectionPolicy {
1897 pub allowed_tiers_for_operation: Vec<(HubOperationClass, Vec<HubTrustTier>)>,
1899 pub default_allowed_tiers: Vec<HubTrustTier>,
1901}
1902
1903impl Default for HubSelectionPolicy {
1904 fn default() -> Self {
1905 Self {
1906 allowed_tiers_for_operation: vec![
1907 (
1908 HubOperationClass::Hello,
1909 vec![HubTrustTier::Full, HubTrustTier::ReadOnly],
1910 ),
1911 (
1912 HubOperationClass::Fetch,
1913 vec![HubTrustTier::Full, HubTrustTier::ReadOnly],
1914 ),
1915 (HubOperationClass::Publish, vec![HubTrustTier::Full]),
1917 (HubOperationClass::Revoke, vec![HubTrustTier::Full]),
1918 (HubOperationClass::TaskClaim, vec![HubTrustTier::Full]),
1919 (HubOperationClass::TaskComplete, vec![HubTrustTier::Full]),
1920 (HubOperationClass::WorkerRegister, vec![HubTrustTier::Full]),
1921 (HubOperationClass::Recipe, vec![HubTrustTier::Full]),
1922 (HubOperationClass::Session, vec![HubTrustTier::Full]),
1923 (HubOperationClass::Dispute, vec![HubTrustTier::Full]),
1924 (HubOperationClass::Swarm, vec![HubTrustTier::Full]),
1925 ],
1926 default_allowed_tiers: vec![HubTrustTier::Full],
1927 }
1928 }
1929}
1930
1931impl HubSelectionPolicy {
1932 pub fn allowed_tiers(&self, operation: &HubOperationClass) -> &[HubTrustTier] {
1934 self.allowed_tiers_for_operation
1935 .iter()
1936 .find(|(op, _)| op == operation)
1937 .map(|(_, tiers)| tiers.as_slice())
1938 .unwrap_or(&self.default_allowed_tiers)
1939 }
1940}