1use chrono::{DateTime, Utc};
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum TaskType {
29 CodeReview,
31 DataPipeline,
33 SupportTicket,
35 DocumentGeneration,
37 Custom,
39}
40
41impl std::fmt::Display for TaskType {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 Self::CodeReview => write!(f, "code_review"),
45 Self::DataPipeline => write!(f, "data_pipeline"),
46 Self::SupportTicket => write!(f, "support_ticket"),
47 Self::DocumentGeneration => write!(f, "document_generation"),
48 Self::Custom => write!(f, "custom"),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum TaskComplexity {
61 Simple,
63 Standard,
65 Complex,
67 Critical,
69}
70
71impl TaskComplexity {
72 fn range_position(self) -> f64 {
74 match self {
75 Self::Simple => 0.0,
76 Self::Standard => 0.33,
77 Self::Complex => 0.66,
78 Self::Critical => 1.0,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(tag = "type", rename_all = "snake_case")]
90pub enum SuccessCriterion {
91 TestsPassed {
93 scope: String,
95 },
96 DataValidated {
98 rule_id: String,
100 },
101 ManualApproval {
103 approver: String,
105 },
106 WebhookConfirmed {
108 url: String,
110 },
111 Custom {
113 description: String,
115 },
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct RefundPolicy {
125 pub auto_refund: bool,
127 pub sla_seconds: u64,
130 pub refund_percentage: u8,
133 pub grace_period_seconds: u64,
135}
136
137impl Default for RefundPolicy {
138 fn default() -> Self {
139 Self {
140 auto_refund: true,
141 sla_seconds: 3600, refund_percentage: 100, grace_period_seconds: 300, }
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct TaskContract {
158 pub contract_id: String,
160 pub task_type: TaskType,
162 pub name: String,
164 pub price_floor_micro_credits: i64,
166 pub price_ceiling_micro_credits: i64,
168 pub success_criteria: Vec<SuccessCriterion>,
170 pub refund_policy: RefundPolicy,
172 pub min_trust_score: f64,
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub custom_label: Option<String>,
178 pub created_at: DateTime<Utc>,
180}
181
182impl TaskContract {
183 pub fn resolve_price(&self, complexity: TaskComplexity, trust_score: f64) -> i64 {
196 let range = self.price_ceiling_micro_credits - self.price_floor_micro_credits;
197 if range <= 0 {
198 return self.price_floor_micro_credits;
199 }
200
201 let complexity_offset = (range as f64 * complexity.range_position()) as i64;
202 let trust_discount = (range as f64 * trust_score.clamp(0.0, 1.0) * 0.2) as i64;
204
205 let price = self.price_floor_micro_credits + complexity_offset - trust_discount;
206 price.clamp(
208 self.price_floor_micro_credits,
209 self.price_ceiling_micro_credits,
210 )
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "snake_case")]
221pub enum TaskOutcome {
222 Success,
224 Failure,
226 PartialSuccess,
228 Timeout,
230 Refunded,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct CriterionResult {
237 pub criterion: SuccessCriterion,
239 pub passed: bool,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub details: Option<String>,
244 pub checked_at: DateTime<Utc>,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct OutcomeVerification {
251 pub task_id: String,
253 pub contract_id: String,
255 pub results: Vec<CriterionResult>,
257 pub outcome: TaskOutcome,
259 pub price_micro_credits: i64,
261 pub verified_at: DateTime<Utc>,
263}
264
265impl OutcomeVerification {
266 pub fn derive_outcome(results: &[CriterionResult]) -> TaskOutcome {
268 if results.is_empty() {
269 return TaskOutcome::Failure;
270 }
271 let passed = results.iter().filter(|r| r.passed).count();
272 let total = results.len();
273 if passed == total {
274 TaskOutcome::Success
275 } else if passed > 0 {
276 TaskOutcome::PartialSuccess
277 } else {
278 TaskOutcome::Failure
279 }
280 }
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct OutcomeRecord {
286 pub task_id: String,
288 pub contract_id: String,
290 pub task_type: TaskType,
292 pub complexity: TaskComplexity,
294 pub agent_id: String,
296 pub agent_trust_score: f64,
298 pub price_micro_credits: i64,
300 pub outcome: TaskOutcome,
302 pub accepted_at: DateTime<Utc>,
304 pub completed_at: DateTime<Utc>,
306 pub refunded: bool,
308 pub refund_amount_micro_credits: i64,
310}
311
312pub fn default_code_review_contract() -> TaskContract {
320 TaskContract {
321 contract_id: "contract-code-review-v1".into(),
322 task_type: TaskType::CodeReview,
323 name: "Code Review".into(),
324 price_floor_micro_credits: 2_000_000,
325 price_ceiling_micro_credits: 5_000_000,
326 success_criteria: vec![
327 SuccessCriterion::TestsPassed {
328 scope: "unit".into(),
329 },
330 SuccessCriterion::ManualApproval {
331 approver: "reviewer".into(),
332 },
333 ],
334 refund_policy: RefundPolicy {
335 sla_seconds: 7200, ..Default::default()
337 },
338 min_trust_score: 0.3,
339 custom_label: None,
340 created_at: Utc::now(),
341 }
342}
343
344pub fn default_data_pipeline_contract() -> TaskContract {
348 TaskContract {
349 contract_id: "contract-data-pipeline-v1".into(),
350 task_type: TaskType::DataPipeline,
351 name: "Data Pipeline Run".into(),
352 price_floor_micro_credits: 5_000_000,
353 price_ceiling_micro_credits: 20_000_000,
354 success_criteria: vec![SuccessCriterion::DataValidated {
355 rule_id: "pipeline-output-schema".into(),
356 }],
357 refund_policy: RefundPolicy {
358 sla_seconds: 3600, ..Default::default()
360 },
361 min_trust_score: 0.5,
362 custom_label: None,
363 created_at: Utc::now(),
364 }
365}
366
367pub fn default_support_ticket_contract() -> TaskContract {
371 TaskContract {
372 contract_id: "contract-support-ticket-v1".into(),
373 task_type: TaskType::SupportTicket,
374 name: "Support Ticket Resolution".into(),
375 price_floor_micro_credits: 500_000,
376 price_ceiling_micro_credits: 2_000_000,
377 success_criteria: vec![SuccessCriterion::Custom {
378 description: "Customer marked ticket as resolved".into(),
379 }],
380 refund_policy: RefundPolicy {
381 sla_seconds: 1800, ..Default::default()
383 },
384 min_trust_score: 0.3,
385 custom_label: None,
386 created_at: Utc::now(),
387 }
388}
389
390pub fn default_document_generation_contract() -> TaskContract {
394 TaskContract {
395 contract_id: "contract-doc-gen-v1".into(),
396 task_type: TaskType::DocumentGeneration,
397 name: "Document Generation".into(),
398 price_floor_micro_credits: 1_000_000,
399 price_ceiling_micro_credits: 10_000_000,
400 success_criteria: vec![SuccessCriterion::DataValidated {
401 rule_id: "document-schema".into(),
402 }],
403 refund_policy: RefundPolicy {
404 sla_seconds: 3600,
405 ..Default::default()
406 },
407 min_trust_score: 0.3,
408 custom_label: None,
409 created_at: Utc::now(),
410 }
411}
412
413#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn task_type_display() {
423 assert_eq!(TaskType::CodeReview.to_string(), "code_review");
424 assert_eq!(TaskType::DataPipeline.to_string(), "data_pipeline");
425 assert_eq!(TaskType::SupportTicket.to_string(), "support_ticket");
426 assert_eq!(
427 TaskType::DocumentGeneration.to_string(),
428 "document_generation"
429 );
430 assert_eq!(TaskType::Custom.to_string(), "custom");
431 }
432
433 #[test]
434 fn task_type_serde_roundtrip() {
435 let tt = TaskType::CodeReview;
436 let json = serde_json::to_string(&tt).unwrap();
437 assert_eq!(json, "\"code_review\"");
438 let back: TaskType = serde_json::from_str(&json).unwrap();
439 assert_eq!(back, tt);
440 }
441
442 #[test]
443 fn resolve_price_simple_no_trust() {
444 let contract = default_code_review_contract();
445 let price = contract.resolve_price(TaskComplexity::Simple, 0.0);
447 assert_eq!(price, 2_000_000);
448 }
449
450 #[test]
451 fn resolve_price_critical_no_trust() {
452 let contract = default_code_review_contract();
453 let price = contract.resolve_price(TaskComplexity::Critical, 0.0);
455 assert_eq!(price, 5_000_000);
456 }
457
458 #[test]
459 fn resolve_price_simple_max_trust() {
460 let contract = default_code_review_contract();
461 let price = contract.resolve_price(TaskComplexity::Simple, 1.0);
463 assert_eq!(price, 2_000_000);
465 }
466
467 #[test]
468 fn resolve_price_critical_max_trust() {
469 let contract = default_code_review_contract();
470 let price = contract.resolve_price(TaskComplexity::Critical, 1.0);
472 assert_eq!(price, 4_400_000);
474 }
475
476 #[test]
477 fn resolve_price_standard_mid_trust() {
478 let contract = default_code_review_contract();
479 let price = contract.resolve_price(TaskComplexity::Standard, 0.5);
481 assert_eq!(price, 2_690_000);
485 }
486
487 #[test]
488 fn resolve_price_flat_range() {
489 let contract = TaskContract {
491 contract_id: "flat".into(),
492 task_type: TaskType::Custom,
493 name: "Flat Price".into(),
494 price_floor_micro_credits: 1_000_000,
495 price_ceiling_micro_credits: 1_000_000,
496 success_criteria: vec![],
497 refund_policy: RefundPolicy::default(),
498 min_trust_score: 0.0,
499 custom_label: Some("flat".into()),
500 created_at: Utc::now(),
501 };
502 assert_eq!(
503 contract.resolve_price(TaskComplexity::Critical, 1.0),
504 1_000_000
505 );
506 }
507
508 #[test]
509 fn derive_outcome_all_pass() {
510 let results = vec![
511 CriterionResult {
512 criterion: SuccessCriterion::TestsPassed {
513 scope: "unit".into(),
514 },
515 passed: true,
516 details: None,
517 checked_at: Utc::now(),
518 },
519 CriterionResult {
520 criterion: SuccessCriterion::DataValidated {
521 rule_id: "schema-1".into(),
522 },
523 passed: true,
524 details: None,
525 checked_at: Utc::now(),
526 },
527 ];
528 assert_eq!(
529 OutcomeVerification::derive_outcome(&results),
530 TaskOutcome::Success
531 );
532 }
533
534 #[test]
535 fn derive_outcome_partial() {
536 let results = vec![
537 CriterionResult {
538 criterion: SuccessCriterion::TestsPassed {
539 scope: "unit".into(),
540 },
541 passed: true,
542 details: None,
543 checked_at: Utc::now(),
544 },
545 CriterionResult {
546 criterion: SuccessCriterion::ManualApproval {
547 approver: "reviewer".into(),
548 },
549 passed: false,
550 details: Some("reviewer rejected".into()),
551 checked_at: Utc::now(),
552 },
553 ];
554 assert_eq!(
555 OutcomeVerification::derive_outcome(&results),
556 TaskOutcome::PartialSuccess
557 );
558 }
559
560 #[test]
561 fn derive_outcome_all_fail() {
562 let results = vec![CriterionResult {
563 criterion: SuccessCriterion::TestsPassed {
564 scope: "e2e".into(),
565 },
566 passed: false,
567 details: Some("3 tests failed".into()),
568 checked_at: Utc::now(),
569 }];
570 assert_eq!(
571 OutcomeVerification::derive_outcome(&results),
572 TaskOutcome::Failure
573 );
574 }
575
576 #[test]
577 fn derive_outcome_empty() {
578 assert_eq!(
579 OutcomeVerification::derive_outcome(&[]),
580 TaskOutcome::Failure
581 );
582 }
583
584 #[test]
585 fn default_contracts_have_valid_ranges() {
586 let contracts = vec![
587 default_code_review_contract(),
588 default_data_pipeline_contract(),
589 default_support_ticket_contract(),
590 default_document_generation_contract(),
591 ];
592 for c in &contracts {
593 assert!(
594 c.price_floor_micro_credits <= c.price_ceiling_micro_credits,
595 "contract {} has floor > ceiling",
596 c.contract_id
597 );
598 assert!(
599 !c.success_criteria.is_empty(),
600 "contract {} has no success criteria",
601 c.contract_id
602 );
603 assert!(c.min_trust_score >= 0.0 && c.min_trust_score <= 1.0);
604 }
605 }
606
607 #[test]
608 fn refund_policy_default() {
609 let policy = RefundPolicy::default();
610 assert!(policy.auto_refund);
611 assert_eq!(policy.sla_seconds, 3600);
612 assert_eq!(policy.refund_percentage, 100);
613 assert_eq!(policy.grace_period_seconds, 300);
614 }
615
616 #[test]
617 fn task_contract_serde_roundtrip() {
618 let contract = default_code_review_contract();
619 let json = serde_json::to_string(&contract).unwrap();
620 let back: TaskContract = serde_json::from_str(&json).unwrap();
621 assert_eq!(back.contract_id, contract.contract_id);
622 assert_eq!(back.task_type, contract.task_type);
623 assert_eq!(
624 back.price_floor_micro_credits,
625 contract.price_floor_micro_credits
626 );
627 }
628
629 #[test]
630 fn outcome_record_serde_roundtrip() {
631 let record = OutcomeRecord {
632 task_id: "task-1".into(),
633 contract_id: "contract-code-review-v1".into(),
634 task_type: TaskType::CodeReview,
635 complexity: TaskComplexity::Standard,
636 agent_id: "agent-1".into(),
637 agent_trust_score: 0.8,
638 price_micro_credits: 3_000_000,
639 outcome: TaskOutcome::Success,
640 accepted_at: Utc::now(),
641 completed_at: Utc::now(),
642 refunded: false,
643 refund_amount_micro_credits: 0,
644 };
645 let json = serde_json::to_string(&record).unwrap();
646 let back: OutcomeRecord = serde_json::from_str(&json).unwrap();
647 assert_eq!(back.task_id, "task-1");
648 assert_eq!(back.outcome, TaskOutcome::Success);
649 }
650
651 #[test]
652 fn support_ticket_pricing_range() {
653 let contract = default_support_ticket_contract();
654 let min = contract.resolve_price(TaskComplexity::Simple, 1.0);
655 let max = contract.resolve_price(TaskComplexity::Critical, 0.0);
656 assert!(min >= 500_000, "min = {min}");
658 assert!(max <= 2_000_000, "max = {max}");
660 }
661
662 #[test]
663 fn data_pipeline_pricing_range() {
664 let contract = default_data_pipeline_contract();
665 let min = contract.resolve_price(TaskComplexity::Simple, 1.0);
666 let max = contract.resolve_price(TaskComplexity::Critical, 0.0);
667 assert!(min >= 5_000_000, "min = {min}");
668 assert!(max <= 20_000_000, "max = {max}");
669 }
670}