1use std::collections::{BTreeMap, BTreeSet};
4
5use rustev_contract::canonical::canonical_bytes;
6use rustev_contract::definition::{ItemSource, RequiredKind, StepBody};
7use rustev_contract::descriptor::{OutputKind, TextStructure};
8use rustev_contract::evidence::EvidenceRecord;
9use rustev_contract::generation::*;
10use rustev_contract::generation_result::*;
11use rustev_contract::ids::ContentDigest;
12use rustev_contract::judgment::{Derivation, Judgment, Lineage, OutValue, Outcome, Unresolved};
13use rustev_contract::output::{RawOutput, TextValue};
14use rustev_contract::plan::{Normalization, PlanExecution, PlanStepDetail};
15use rustev_contract::request::RequestDoc;
16use rustev_contract::schema;
17use rustev_contract::scope::Scope;
18
19use crate::Compiled;
20use crate::evaluate::{NotReady, RequestIdentityError, Supplied, SupplyError};
21use crate::generation::Checked;
22use crate::template as t;
23use crate::value::Value;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum GenerationError {
27 NotReady(NotReady),
28 InvalidState(String),
30}
31impl std::fmt::Display for GenerationError {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 write!(f, "generation: {self:?}")
34 }
35}
36impl std::error::Error for GenerationError {}
37fn bad(s: impl ToString) -> GenerationError {
38 GenerationError::InvalidState(s.to_string())
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct GenerationRequest {
44 pub backend_id: String,
45 pub owner: String,
46 pub screen_projection: Option<Vec<u8>>,
47 pub document: GenerateRequest,
48}
49struct Item {
50 step: String,
51 declaration: Generate,
52 result: TextItem,
53 backstops: BTreeMap<String, TextField>,
54 pending: Option<GenerationRequest>,
55 round: u64,
56 screens: Vec<GenerationRequest>,
57 screening: BTreeMap<String, rustev_contract::screening::ScreeningEvidence>,
58}
59
60pub struct GenerationEvaluation<'c> {
63 compiled: &'c Compiled,
64 judgment: Judgment,
65 evidence: EvidenceRecord,
66 steps: Vec<TextStep>,
67 items: Vec<Item>,
68 notices: Vec<TextNotice>,
69 remaining: u64,
70}
71impl<'c> GenerationEvaluation<'c> {
72 pub(crate) fn start(
73 compiled: &'c Compiled,
74 judgment: Judgment,
75 evidence: EvidenceRecord,
76 values: (BTreeMap<String, Value>, BTreeMap<String, Value>),
77 context: (
78 BTreeMap<String, Lineage>,
79 crate::evaluate::GenerationScreening,
80 ),
81 issued: u64,
82 ) -> Result<Self, GenerationError> {
83 let (inputs, exact) = values;
84 let (lineage, screening) = context;
85 let mut stage = Self {
86 compiled,
87 judgment,
88 evidence,
89 steps: vec![],
90 items: vec![],
91 notices: vec![],
92 remaining: compiled
93 .plan
94 .definition
95 .limits
96 .max_semantic_requests
97 .saturating_sub(issued),
98 };
99 let mut roots = BTreeMap::from([
100 ("input".into(), Value::Record(inputs)),
101 ("step".into(), Value::Record(exact)),
102 ]);
103 if let Outcome::Propose { params, .. } = &stage.judgment.outcome {
104 roots.insert(
105 "proposal".into(),
106 Value::Record(
107 params
108 .iter()
109 .map(|(n, v)| Ok((n.clone(), from_out(v)?)))
110 .collect::<Result<_, GenerationError>>()?,
111 ),
112 );
113 }
114 for step in &compiled.plan.definition.steps {
115 let StepBody::Generate(g) = &step.body else {
116 continue;
117 };
118 let checked = compiled
119 .info
120 .generation
121 .get(&step.id)
122 .ok_or_else(|| bad("missing compiled templates"))?;
123 let matches = match &stage.judgment.outcome {
124 Outcome::Propose { action, .. } => match &g.when {
125 GenerateWhen::AnyPropose => true,
126 GenerateWhen::Actions(actions) => actions.contains(action),
127 },
128 _ => false,
129 };
130 let mut result = TextStep {
131 step: step.id.clone(),
132 state: GenerationState::Ran,
133 lineage: stage.judgment.lineage.clone(),
134 items: vec![],
135 text_origin: TextOrigin::None,
136 counts: TextCounts::default(),
137 };
138 if !matches {
139 result.state = GenerationState::Skipped {
140 reason: GenerationSkipReason::OutcomeNotMatched,
141 };
142 } else {
143 match make_items(&step.id, g, checked, &roots, &lineage) {
144 Ok(mut items) => {
145 for item in &mut items {
146 if item.pending.is_some()
147 && let Some(reason) = screening.reason(
148 g,
149 &item.result.id,
150 &stage.judgment,
151 &compiled.plan.definition,
152 )
153 {
154 complete(item, reason);
155 }
156 if item.pending.is_some() {
157 if stage.remaining == 0 {
158 complete(item, TextReason::BudgetExhausted);
159 } else {
160 stage.remaining -= 1;
161 }
162 }
163 }
164 stage.items.extend(items);
165 }
166 Err(u) => {
167 result.state = GenerationState::Skipped {
168 reason: GenerationSkipReason::SourceUnresolved(u),
169 }
170 }
171 }
172 }
173 stage.steps.push(result);
174 }
175 Ok(stage)
176 }
177
178 pub fn judgment(&self) -> &Judgment {
180 &self.judgment
181 }
182
183 pub fn pending(&self) -> Vec<GenerationRequest> {
185 self.items
186 .iter()
187 .flat_map(|i| {
188 if i.screens.is_empty() {
189 i.pending.clone().into_iter().collect()
190 } else {
191 i.screens.clone()
192 }
193 })
194 .collect()
195 }
196
197 pub fn request_document(
198 &self,
199 step: &str,
200 instance: &[String],
201 target: u32,
202 scope: &Scope,
203 ) -> Result<RequestDoc, RequestIdentityError> {
204 if let Some((_, r)) = self.find_screen(step, instance) {
205 let (_, b) =
206 output_screen_binding(&self.compiled.plan, step).expect("bound output screener");
207 if target != 0 {
208 return Err(RequestIdentityError::UnknownTarget {
209 step: step.into(),
210 target,
211 });
212 }
213 return Ok(RequestDoc {
214 schema: schema::REQUEST.into(),
215 scope: scope.clone(),
216 backend_id: b.backend_id.clone(),
217 artifact: b.artifact.clone(),
218 descriptor: b.descriptor.clone(),
219 projection: String::from_utf8(
220 r.screen_projection.clone().expect("screen projection"),
221 )
222 .map_err(|_| RequestIdentityError::ProjectionNotUtf8 { step: step.into() })?,
223 output: OutputKind::Screen,
224 requires: RequiredKind::ScreenVerdict,
225 normalization: Normalization::None,
226 });
227 }
228 let missing = || RequestIdentityError::NotPending {
229 step: step.into(),
230 instance: instance.to_vec(),
231 };
232 let item = self.find(step, instance).ok_or_else(missing)?;
233 let request = item.pending.as_ref().ok_or_else(missing)?;
234 let binding = self
235 .compiled
236 .plan
237 .steps
238 .iter()
239 .find_map(|s| match &s.detail {
240 PlanStepDetail::Generate(b) if s.id == step => Some(b),
241 _ => None,
242 })
243 .ok_or_else(missing)?;
244 let unknown = || RequestIdentityError::UnknownTarget {
245 step: step.into(),
246 target,
247 };
248 let (backend_id, artifact, descriptor) = if target == 0 {
249 (&binding.backend_id, &binding.artifact, &binding.descriptor)
250 } else {
251 let PlanExecution::Declared(e) = &self.compiled.plan.execution else {
252 return Err(unknown());
253 };
254 let f = e
255 .fallbacks
256 .iter()
257 .find(|f| f.step == step && f.target == target)
258 .ok_or_else(unknown)?;
259 (&f.backend_id, &f.artifact, &f.descriptor)
260 };
261 let envelope = serde_json::json!({"operation":"generate", "plan_id":self.compiled.id, "request":request.document});
264 let projection = canonical_bytes(&envelope)
265 .ok()
266 .and_then(|b| String::from_utf8(b).ok())
267 .ok_or_else(|| RequestIdentityError::ProjectionNotUtf8 { step: step.into() })?;
268 let max_output_bytes = request
269 .document
270 .output
271 .fields
272 .iter()
273 .map(|f| match &f.ty {
274 GeneratedType::Text { max_bytes, .. } => *max_bytes,
275 GeneratedType::List {
276 vocabulary,
277 max_items,
278 } => {
279 max_items.saturating_mul(
282 binding
283 .vocabularies
284 .get(vocabulary)
285 .into_iter()
286 .flatten()
287 .map(|s| s.len() as u64)
288 .max()
289 .unwrap_or(0),
290 )
291 }
292 })
293 .fold(0u64, u64::saturating_add);
294 Ok(RequestDoc {
295 schema: schema::REQUEST.into(),
296 scope: scope.clone(),
297 backend_id: backend_id.clone(),
298 artifact: artifact.clone(),
299 descriptor: descriptor.clone(),
300 projection,
301 output: OutputKind::TextRecord {
302 structure: TextStructure::JsonSchema,
303 max_output_bytes,
304 languages: vec![request.document.language.clone()],
305 streaming: false,
306 },
307 requires: RequiredKind::TextRecord,
308 normalization: Normalization::None,
309 })
310 }
311
312 fn find_screen(&self, step: &str, instance: &[String]) -> Option<(&Item, &GenerationRequest)> {
313 self.items.iter().find_map(|i| {
314 i.screens
315 .iter()
316 .find(|r| r.document.step == step && r.document.instance == instance)
317 .map(|r| (i, r))
318 })
319 }
320
321 fn screen_check(&self, step: &str, raw: &RawOutput) -> Result<(), Unresolved> {
322 let invalid = || Unresolved::InvalidBackendOutput {
323 detail: "invalid output screening verdict".into(),
324 };
325 let (_, b) = output_screen_binding(&self.compiled.plan, step).ok_or_else(invalid)?;
326 let declared = self
327 .compiled
328 .info
329 .screen_categories
330 .get(&b.backend_id)
331 .ok_or_else(invalid)?;
332 match raw {
333 RawOutput::Screen { categories, .. }
334 if categories.iter().collect::<BTreeSet<_>>().len() == categories.len()
335 && categories.iter().all(|c| declared.contains(c)) =>
336 {
337 Ok(())
338 }
339 _ => Err(invalid()),
340 }
341 }
342
343 fn supply_screen(
344 &mut self,
345 step: &str,
346 instance: &[String],
347 supplied: Supplied,
348 ) -> Result<(), GenerationError> {
349 use rustev_contract::screening::{
350 ScreenConsequence, ScreenVerdict, ScreeningEvidence, screen_text_digest,
351 };
352 let supplied = match supplied {
353 Supplied::Output(raw) => match self.screen_check(step, &raw) {
354 Ok(()) => Supplied::Output(raw),
355 Err(u) => Supplied::Failed(u),
356 },
357 other => other,
358 };
359 let (_, binding) = output_screen_binding(&self.compiled.plan, step)
360 .ok_or_else(|| bad("unbound screener"))?;
361 let item = self
362 .items
363 .iter_mut()
364 .find(|i| {
365 i.screens
366 .iter()
367 .any(|r| r.document.step == step && r.document.instance == instance)
368 })
369 .ok_or_else(|| bad("screen request is not pending"))?;
370 let index = item
371 .screens
372 .iter()
373 .position(|r| r.document.step == step)
374 .ok_or_else(|| bad("missing screen"))?;
375 let request = item.screens.remove(index);
376 let name = &request.document.output.fields[0].name;
377 let Some(TextValue::Text(text)) = item.result.generated[name].value.as_ref() else {
378 return Err(bad("missing screened text"));
379 };
380 let digest = screen_text_digest(text);
381 let (verdict, mut categories, consequence, reason) = match supplied {
382 Supplied::Output(RawOutput::Screen {
383 blocked: false,
384 categories,
385 ..
386 }) => (
387 ScreenVerdict::Pass,
388 categories,
389 ScreenConsequence::None,
390 None,
391 ),
392 Supplied::Output(RawOutput::Screen { categories, .. }) => (
393 ScreenVerdict::Blocked,
394 categories,
395 ScreenConsequence::DropField,
396 Some(TextReason::ScreenBlocked),
397 ),
398 Supplied::Failed(reason) => (
399 ScreenVerdict::Unavailable {
400 reason: match reason {
401 Unresolved::BackendUnavailable { .. } => Unresolved::BackendUnavailable {
402 detail: "output screener unavailable".into(),
403 },
404 Unresolved::InvalidBackendOutput { .. } => {
405 Unresolved::InvalidBackendOutput {
406 detail: "invalid output screening verdict".into(),
407 }
408 }
409 other => other,
410 },
411 },
412 vec![],
413 ScreenConsequence::FailClosed,
414 Some(TextReason::ScreenUnavailable),
415 ),
416 _ => return Err(bad("unchecked screen verdict")),
417 };
418 categories.sort();
419 item.screening.insert(
420 name.clone(),
421 ScreeningEvidence {
422 entry: item.step.clone(),
423 field: format!("output:{}/{name}", item.step),
424 item: item.result.id.first().cloned(),
425 text_digest: digest,
426 verdict,
427 categories,
428 consequence,
429 backend_id: binding.backend_id.clone(),
430 artifact: binding.artifact.clone(),
431 descriptor: binding.descriptor.clone(),
432 step: step.into(),
433 },
434 );
435 if let Some(reason) = reason {
436 item.result
438 .generated
439 .insert(name.clone(), fallback_field(item, name, reason));
440 }
441 Ok(())
442 }
443
444 fn find(&self, step: &str, instance: &[String]) -> Option<&Item> {
445 self.items.iter().find(|i| {
446 i.step == step
447 && i.pending
448 .as_ref()
449 .is_some_and(|r| r.document.instance == instance)
450 })
451 }
452
453 pub fn check_output(
456 &self,
457 step: &str,
458 instance: &[String],
459 raw: &RawOutput,
460 ) -> Result<Result<(), Unresolved>, SupplyError> {
461 if self.find_screen(step, instance).is_some() {
462 return Ok(self.screen_check(step, raw));
463 }
464 let item = self
465 .find(step, instance)
466 .ok_or_else(|| SupplyError::NotPending {
467 step: step.into(),
468 instance: instance.to_vec(),
469 })?;
470 Ok(record_check(item, raw))
471 }
472
473 pub fn check_attempt_output(
476 &mut self,
477 step: &str,
478 instance: &[String],
479 raw: &RawOutput,
480 ) -> Result<Result<(), Unresolved>, SupplyError> {
481 let checked = self.check_output(step, instance, raw)?;
482 if checked.is_err()
483 && let Some(item) = self.find(step, instance)
484 && let RawOutput::TextRecord(fields) = raw
485 {
486 let notices: Vec<_> = fields
487 .keys()
488 .filter(|n| !item.declaration.output.fields.iter().any(|f| &f.name == *n))
489 .map(|name| TextNotice {
490 kind: TextNoticeKind::OwnershipViolation,
491 step: step.into(),
492 instance: item.result.id.clone(),
493 field: name.clone(),
494 })
495 .collect();
496 for notice in notices {
497 if !self.notices.contains(¬ice) {
498 self.notices.push(notice);
499 }
500 }
501 }
502 Ok(checked)
503 }
504
505 pub fn supply(
507 &mut self,
508 step: &str,
509 instance: &[String],
510 supplied: Supplied,
511 ) -> Result<(), GenerationError> {
512 self.supply_attempt(
513 step,
514 instance,
515 supplied,
516 &format!("{step}:{instance:?}:1"),
517 1,
518 )
519 }
520
521 pub fn supply_attempt(
524 &mut self,
525 step: &str,
526 instance: &[String],
527 supplied: Supplied,
528 attempt: &str,
529 attempts: u32,
530 ) -> Result<(), GenerationError> {
531 if attempt.is_empty() || attempts == 0 {
532 return Err(bad("attempt identity and count required"));
533 }
534 if self.find_screen(step, instance).is_some() {
535 return self.supply_screen(step, instance, supplied);
536 }
537 let failure = if let Supplied::Failed(u) = &supplied {
538 Some(failure_reason(u, attempts)?)
539 } else {
540 None
541 };
542 let item = self
543 .items
544 .iter_mut()
545 .find(|i| {
546 i.step == step
547 && i.pending
548 .as_ref()
549 .is_some_and(|r| r.document.instance == instance)
550 })
551 .ok_or_else(|| bad("request is not pending"))?;
552 let request = item
553 .pending
554 .as_ref()
555 .ok_or_else(|| bad("request is not pending"))?
556 .clone();
557 if let Some(reason) = failure {
558 complete(item, reason);
559 return Ok(());
560 }
561 let Supplied::Output(raw) = supplied else {
562 return Err(bad("missing supply"));
563 };
564 if record_check(item, &raw).is_err() {
565 if let RawOutput::TextRecord(fields) = &raw {
566 for name in fields
567 .keys()
568 .filter(|n| !item.declaration.output.fields.iter().any(|f| &f.name == *n))
569 {
570 self.notices.push(TextNotice {
571 kind: TextNoticeKind::OwnershipViolation,
572 step: step.into(),
573 instance: item.result.id.clone(),
574 field: name.clone(),
575 });
576 }
577 }
578 complete(item, TextReason::InvalidOutput);
579 return Ok(());
580 }
581 let RawOutput::TextRecord(fields) = raw else {
582 return Err(bad("record check disagreed"));
583 };
584 let mut unavailable_screens = Vec::new();
585 let mut invalid = Vec::new();
586 for field in &item.declaration.output.fields {
587 if item.result.generated.contains_key(&field.name) {
588 continue;
589 }
590 match validate_field(
591 field,
592 fields.get(&field.name),
593 &request.document.vocabularies,
594 ) {
595 Ok(value) => {
596 item.result.generated.insert(
597 field.name.clone(),
598 TextField {
599 origin: if item.round == 0 {
600 FieldOrigin::Generated
601 } else {
602 FieldOrigin::Repaired
603 },
604 truncated: false,
605 source: TextSource::Fresh,
606 value: Some(value.clone()),
607 derivation: Some(Derivation::ModelDerived),
608 lineage: Some(TextLineage::Model {
609 step: step.into(),
610 template: request.document.template_id.clone(),
611 projection: item.declaration.projection.clone(),
612 attempt: attempt.into(),
613 }),
614 },
615 );
616 if matches!(field.ty, GeneratedType::Text { .. })
617 && matches!(
618 item.declaration.screen.output,
619 ScreenOutput::Screener { .. }
620 )
621 {
622 let screen_step = output_screen_step(step, &field.name);
623 let (_, b) = output_screen_binding(&self.compiled.plan, &screen_step)
624 .ok_or_else(|| bad("unbound output screener"))?;
625 let TextValue::Text(text) = value else {
626 return Err(bad("invalid text"));
627 };
628 let mut document = request.document.clone();
629 document.step = screen_step;
630 document.instance = item.result.id.clone();
631 document.repair = RepairRequest::None;
632 document.output.fields = vec![field.clone()];
633 if self.remaining == 0 {
634 unavailable_screens
635 .push((document.step.clone(), document.instance.clone()));
636 }
637 item.screens.push(GenerationRequest {
638 backend_id: b.backend_id.clone(),
639 owner: step.into(),
640 document,
641 screen_projection: Some(
642 canonical_bytes(&crate::compile::screening::projection(
643 serde_json::json!(text),
644 ))
645 .map_err(bad)?,
646 ),
647 });
648 self.remaining = self.remaining.saturating_sub(1);
649 }
650 }
651 Err(reason) => invalid.push(RepairField {
652 name: field.name.clone(),
653 reason,
654 }),
655 }
656 }
657 if invalid.is_empty() {
658 item.pending = None;
659 } else if item.round < item.declaration.repair.max_attempts && self.remaining == 0 {
660 complete(item, TextReason::BudgetExhausted);
661 } else if item.round < item.declaration.repair.max_attempts {
662 self.remaining -= 1;
663 item.round += 1;
664 let mut next = request;
665 next.document.instance = item.result.id.clone();
666 next.document
667 .instance
668 .push(format!("repair:{}", item.round));
669 next.document.repair = RepairRequest::Round {
672 round: item.round,
673 fields: invalid.clone(),
674 };
675 next.document.output = item.declaration.output.clone();
676 if item.declaration.repair.only_missing_fields {
677 next.document
678 .output
679 .fields
680 .retain(|f| invalid.iter().any(|i| i.name == f.name));
681 }
682 item.pending = Some(next);
683 } else {
684 complete(item, TextReason::InvalidOutput);
685 }
686 for (step, instance) in unavailable_screens {
687 self.supply_screen(
688 &step,
689 &instance,
690 Supplied::Failed(Unresolved::BudgetExhausted {
691 resource: "max_semantic_requests".into(),
692 }),
693 )?;
694 }
695 Ok(())
696 }
697
698 pub fn note_cached_attempt(&mut self, attempt: &str, decision: &str) {
701 for item in &mut self.items {
702 for field in item.result.generated.values_mut() {
703 if matches!(&field.lineage, Some(TextLineage::Model { attempt: a, .. }) if a == attempt)
704 {
705 field.source = TextSource::ServedFromCache {
706 original_decision_id: decision.into(),
707 };
708 }
709 }
710 }
711 }
712
713 pub fn withhold(
716 &mut self,
717 step: &str,
718 instance: &[String],
719 reason: TextReason,
720 ) -> Result<(), GenerationError> {
721 let item = self
722 .items
723 .iter_mut()
724 .find(|i| {
725 i.step == step
726 && i.pending
727 .as_ref()
728 .is_some_and(|r| r.document.instance == instance)
729 })
730 .ok_or_else(|| bad("request is not pending"))?;
731 complete(item, reason);
732 Ok(())
733 }
734
735 pub fn verify_ownership(&self, judgment: &mut Judgment, text: &mut TextResult) {
738 if judgment != &self.judgment {
739 *judgment = self.judgment.clone();
740 text.notices.push(TextNotice {
741 kind: TextNoticeKind::OwnershipRestored,
742 step: String::new(),
743 instance: vec![],
744 field: "judgment".into(),
745 });
746 }
747 for step in &mut text.steps {
748 for item in &mut step.items {
749 if let Some(source) = self
750 .items
751 .iter()
752 .find(|i| i.step == step.step && i.result.id == item.id)
753 {
754 let names: BTreeSet<_> = item
755 .context
756 .keys()
757 .chain(source.result.context.keys())
758 .cloned()
759 .collect();
760 for name in names {
761 if item.context.get(&name) != source.result.context.get(&name) {
762 text.notices.push(TextNotice {
763 kind: TextNoticeKind::OwnershipRestored,
764 step: step.step.clone(),
765 instance: item.id.clone(),
766 field: name,
767 });
768 }
769 }
770 item.context = source.result.context.clone();
771 }
772 }
773 }
774 }
775
776 pub fn finish(
777 &self,
778 ) -> Result<(Judgment, EvidenceRecord, Option<TextResult>), GenerationError> {
779 let pending = self.pending().len();
780 if pending != 0 {
781 return Err(GenerationError::NotReady(NotReady { pending }));
782 }
783 if self.steps.is_empty() {
784 return Ok((self.judgment.clone(), self.evidence.clone(), None));
785 }
786 let mut text = TextResult {
787 schema: schema::TEXT_RESULT.into(),
788 decision_id: self.evidence.decision_id.clone(),
789 plan_id: self.compiled.id.clone(),
790 steps: self.steps.clone(),
791 text_origin: TextOrigin::None,
792 counts: TextCounts::default(),
793 notices: self.notices.clone(),
794 };
795 for step in &mut text.steps {
796 step.items = self
797 .items
798 .iter()
799 .filter(|i| i.step == step.step)
800 .map(|i| i.result.clone())
801 .collect();
802 for item in &step.items {
803 for (name, field) in &item.generated {
804 if field.truncated {
805 text.notices.push(TextNotice {
806 kind: TextNoticeKind::BackstopTruncated,
807 step: step.step.clone(),
808 instance: item.id.clone(),
809 field: name.clone(),
810 });
811 }
812 }
813 }
814 (step.text_origin, step.counts) =
815 summarize(step.items.iter().flat_map(|i| i.generated.values()));
816 }
817 (text.text_origin, text.counts) = summarize(
818 text.steps
819 .iter()
820 .flat_map(|s| s.items.iter().flat_map(|i| i.generated.values())),
821 );
822 let mut judgment = self.judgment.clone();
823 self.verify_ownership(&mut judgment, &mut text);
824 let mut evidence_record = self.evidence.clone();
825 let screening: Vec<_> = self
826 .items
827 .iter()
828 .flat_map(|i| i.screening.values().cloned())
829 .collect();
830 if !screening.is_empty() {
831 evidence_record
832 .screening
833 .get_or_insert_with(Vec::new)
834 .extend(screening);
835 }
836 evidence_record.generation = Some(
837 text.steps
838 .iter()
839 .map(|s| evidence(s, &text.notices))
840 .collect::<Result<_, _>>()?,
841 );
842 Ok((judgment, evidence_record, Some(text)))
843 }
844}
845
846fn failure_reason(u: &Unresolved, attempts: u32) -> Result<TextReason, GenerationError> {
847 Ok(match u {
848 Unresolved::InvalidBackendOutput { .. } => TextReason::InvalidOutput,
849 Unresolved::DeadlineExceeded => TextReason::Deadline,
850 Unresolved::BudgetExhausted { .. } => TextReason::BudgetExhausted,
851 Unresolved::BackendUnavailable { .. } if attempts >= 2 => TextReason::RetriesExhausted,
852 Unresolved::BackendUnavailable { .. } => TextReason::BackendUnavailable,
853 _ => return Err(bad("not a runtime failure")),
854 })
855}
856fn record_check(item: &Item, raw: &RawOutput) -> Result<(), Unresolved> {
857 let RawOutput::TextRecord(fields) = raw else {
858 return Err(Unresolved::InvalidBackendOutput {
859 detail: "expected text_record".into(),
860 });
861 };
862 let Some(request) = &item.pending else {
863 return Err(Unresolved::InvalidBackendOutput {
864 detail: "no pending request".into(),
865 });
866 };
867 if fields
868 .keys()
869 .any(|n| !request.document.output.fields.iter().any(|f| &f.name == n))
870 {
871 return Err(Unresolved::InvalidBackendOutput {
872 detail: "undeclared or unrequested member".into(),
873 });
874 }
875 Ok(())
876}
877pub fn validate_field<'a>(
879 field: &GeneratedField,
880 value: Option<&'a TextValue>,
881 vocabularies: &BTreeMap<String, Vec<String>>,
882) -> Result<&'a TextValue, FieldInvalid> {
883 let value = value.ok_or(FieldInvalid::Missing)?;
884 match (&field.ty, value) {
885 (GeneratedType::Text { max_bytes, lines }, TextValue::Text(s)) => {
886 if s.is_empty() {
887 return Err(FieldInvalid::Empty);
888 }
889 if s.len() as u64 > *max_bytes {
890 return Err(FieldInvalid::TooLong);
891 }
892 if s.chars()
893 .any(|c| c.is_control() && !(c == '\n' && *lines == Lines::Multi))
894 {
895 return Err(FieldInvalid::ControlCharacter);
896 }
897 }
898 (
899 GeneratedType::List {
900 vocabulary,
901 max_items,
902 },
903 TextValue::List(v),
904 ) => {
905 if field.required && v.is_empty() {
906 return Err(FieldInvalid::Empty);
907 }
908 if v.len() as u64 > *max_items {
909 return Err(FieldInvalid::TooManyItems);
910 }
911 let options = vocabularies
912 .get(vocabulary)
913 .ok_or(FieldInvalid::NotInVocabulary)?;
914 if v.iter().any(|s| !options.contains(s)) {
915 return Err(FieldInvalid::NotInVocabulary);
916 }
917 if v.iter().collect::<BTreeSet<_>>().len() != v.len() {
918 return Err(FieldInvalid::DuplicateItem);
919 }
920 }
921 _ => return Err(FieldInvalid::WrongType),
922 }
923 Ok(value)
924}
925fn fallback_field(item: &Item, name: &str, reason: TextReason) -> TextField {
926 let mut slot = item.backstops.get(name).cloned().unwrap_or(TextField {
927 origin: FieldOrigin::Absent { reason },
928 truncated: false,
929 source: TextSource::Fresh,
930 value: None,
931 derivation: None,
932 lineage: None,
933 });
934 if slot.value.is_some() {
935 slot.origin = FieldOrigin::Backstop { reason };
936 }
937 slot
938}
939fn complete(item: &mut Item, reason: TextReason) {
940 for field in &item.declaration.output.fields {
941 let fallback = fallback_field(item, &field.name, reason);
942 item.result
943 .generated
944 .entry(field.name.clone())
945 .or_insert(fallback);
946 }
947 item.pending = None;
948}
949fn summarize<'a>(fields: impl Iterator<Item = &'a TextField>) -> (TextOrigin, TextCounts) {
950 let mut c = TextCounts::default();
951 for f in fields {
952 match f.origin {
953 FieldOrigin::Generated => c.generated += 1,
954 FieldOrigin::Repaired => c.repaired += 1,
955 FieldOrigin::Backstop { reason } => {
956 c.backstop += 1;
957 *c.reasons.entry(reason).or_default() += 1;
958 }
959 FieldOrigin::Absent { reason } => {
960 c.absent += 1;
961 *c.reasons.entry(reason).or_default() += 1;
962 }
963 }
964 }
965 let origin = if c.generated + c.repaired + c.backstop == 0 {
966 TextOrigin::None
967 } else if c.backstop + c.absent == 0 {
968 TextOrigin::Generated
969 } else if c.generated + c.repaired == 0 {
970 TextOrigin::Backstop
971 } else {
972 TextOrigin::Partial
973 };
974 (origin, c)
975}
976fn digest<T: serde::Serialize>(value: &T) -> Result<ContentDigest, GenerationError> {
977 rustev_contract::generation_result::generation_value_digest(value).map_err(bad)
978}
979fn evidence(
980 step: &TextStep,
981 notices: &[TextNotice],
982) -> Result<GenerationEvidence, GenerationError> {
983 Ok(GenerationEvidence {
984 step: step.step.clone(),
985 state: step.state.clone(),
986 text_origin: step.text_origin,
987 counts: step.counts.clone(),
988 notices: notices
989 .iter()
990 .filter(|n| n.step == step.step)
993 .cloned()
994 .collect(),
995 items: step
996 .items
997 .iter()
998 .map(|i| {
999 Ok(GenerationItemEvidence {
1000 id: i.id.clone(),
1001 context: i
1002 .context
1003 .iter()
1004 .map(|(n, v)| Ok((n.clone(), digest(&v.value)?)))
1005 .collect::<Result<_, GenerationError>>()?,
1006 generated: i
1007 .generated
1008 .iter()
1009 .map(|(n, f)| {
1010 Ok((
1011 n.clone(),
1012 TextFieldEvidence {
1013 origin: f.origin.clone(),
1014 truncated: f.truncated,
1015 source: f.source.clone(),
1016 value_digest: f.value.as_ref().map(digest).transpose()?,
1017 byte_length: f.value.as_ref().map(|v| match v {
1018 TextValue::Text(s) => s.len() as u64,
1019 TextValue::List(v) => {
1020 v.iter().map(|s| s.len() as u64).sum()
1021 }
1022 }),
1023 },
1024 ))
1025 })
1026 .collect::<Result<_, GenerationError>>()?,
1027 })
1028 })
1029 .collect::<Result<_, GenerationError>>()?,
1030 })
1031}
1032
1033fn reference<'a>(r: &str, roots: &'a BTreeMap<String, Value>) -> Result<&'a Value, Unresolved> {
1034 let missing = || Unresolved::missing([r.to_string()]);
1035 let (root, rest) = r.split_once(':').ok_or_else(missing)?;
1036 let mut parts = rest.split('/');
1037 let mut value = if root == "bind" {
1038 roots
1039 .get(parts.next().ok_or_else(missing)?)
1040 .ok_or_else(missing)?
1041 } else {
1042 roots.get(root).ok_or_else(missing)?
1043 };
1044 for part in parts {
1045 let Value::Record(fields) = value else {
1046 return Err(missing());
1047 };
1048 value = fields.get(part).ok_or_else(missing)?;
1049 }
1050 if matches!(value, Value::Null | Value::Absent(_)) {
1051 return Err(missing());
1052 }
1053 Ok(value)
1054}
1055fn from_out(v: &OutValue) -> Result<Value, GenerationError> {
1056 Ok(match v {
1057 OutValue::Attributed { value, .. } => return from_out(value),
1058 OutValue::Bool(v) => Value::Bool(*v),
1059 OutValue::Integer(v) => Value::Integer(*v),
1060 OutValue::Decimal(v) => Value::Decimal(*v),
1061 OutValue::Text(v) => Value::Text(v.clone()),
1062 OutValue::Enum(v) => Value::Enum(v.clone()),
1063 OutValue::Timestamp(v) => Value::Timestamp(*v),
1064 OutValue::Duration(v) => Value::Duration(*v),
1065 OutValue::None => Value::Maybe(None),
1066 OutValue::List(v) => Value::List(v.iter().map(from_out).collect::<Result<_, _>>()?),
1067 OutValue::Record(v) => Value::Record(
1068 v.iter()
1069 .map(|(n, v)| Ok((n.clone(), from_out(v)?)))
1070 .collect::<Result<_, GenerationError>>()?,
1071 ),
1072 OutValue::Filtered(v) => Value::Filtered(v.clone()),
1073 OutValue::Shortlist(v) => Value::Shortlist(v.clone()),
1074 OutValue::Ranking(v) => Value::Ranking(v.clone()),
1075 OutValue::Selection(_) => {
1076 return Err(GenerationError::InvalidState(
1077 "selection is not a generation input".into(),
1078 ));
1079 }
1080 })
1081}
1082pub(crate) fn template_value(v: &Value) -> Option<t::Value> {
1085 Some(match v {
1086 Value::Bool(v) => t::Value::Bool(*v),
1087 Value::Integer(v) => t::Value::Integer(*v),
1088 Value::Decimal(v) => t::Value::Decimal(*v),
1089 Value::Text(v) => t::Value::Text(v.clone()),
1090 Value::Enum(v) => t::Value::Enum(v.clone()),
1091 Value::Timestamp(v) => t::Value::Timestamp(*v),
1092 Value::Maybe(v) => t::Value::Maybe(match v {
1093 Some(v) => Some(Box::new(template_value(v)?)),
1094 None => None,
1095 }),
1096 Value::Null | Value::Absent(_) => t::Value::Maybe(None),
1097 Value::List(v) => t::Value::List(v.iter().map(template_value).collect::<Option<_>>()?),
1098 Value::Record(v) => t::Value::Record(
1099 v.iter()
1100 .filter_map(|(n, v)| template_value(v).map(|v| (n.clone(), v)))
1101 .collect(),
1102 ),
1103 Value::Filtered(_) | Value::Shortlist(_) => {
1104 t::Value::List(v.ids()?.into_iter().map(t::Value::Text).collect())
1105 }
1106 _ => return None,
1107 })
1108}
1109fn make_items(
1110 id: &str,
1111 g: &Generate,
1112 checked: &Checked,
1113 roots: &BTreeMap<String, Value>,
1114 lineage: &BTreeMap<String, Lineage>,
1115) -> Result<Vec<Item>, Unresolved> {
1116 let invalid = || Unresolved::invalid([], "generation source cannot be rendered");
1117 let instances = match &g.for_each {
1118 ModelForEach::None => vec![vec![]],
1119 ModelForEach::List(f) => reference(&f.over, roots)?
1120 .ids()
1121 .ok_or_else(invalid)?
1122 .into_iter()
1123 .map(|id| vec![id])
1124 .collect(),
1125 };
1126 let mut items = Vec::new();
1127 for (index, instance) in instances.into_iter().enumerate() {
1128 let mut roots = roots.clone();
1129 if let ModelForEach::List(f) = &g.for_each {
1130 let item_id = instance.first().ok_or_else(invalid)?;
1131 let value = match &f.items {
1132 ItemSource::None => Value::Text(item_id.clone()),
1133 ItemSource::List { list, id_field } => {
1134 let Value::List(values) = reference(list, &roots)? else {
1135 return Err(invalid());
1136 };
1137 values.iter().find(|v| matches!(v, Value::Record(m) if matches!(m.get(id_field), Some(Value::Text(s) | Value::Enum(s)) if s == item_id))).cloned().ok_or_else(invalid)?
1138 }
1139 };
1140 roots.insert(f.bind.clone(), value);
1141 }
1142 let mut params = BTreeMap::new();
1143 for (name, binding) in &g.template.params {
1144 let value = match binding {
1145 TemplateBinding::Reference(r) => reference(r, &roots)?.clone(),
1146 TemplateBinding::Literal { literal } => Value::from_literal(literal),
1147 TemplateBinding::Parameter(_) => return Err(invalid()),
1148 };
1149 params.insert(name.clone(), template_value(&value).ok_or_else(invalid)?);
1150 }
1151 let instructions = checked
1152 .prompt
1153 .as_ref()
1154 .ok_or_else(invalid)?
1155 .render(&BTreeMap::from([(
1156 "param".into(),
1157 t::Value::Record(params),
1158 )]))
1159 .map_err(|_| invalid())?
1160 .text;
1161 let values = roots
1162 .iter()
1163 .filter_map(|(n, v)| template_value(v).map(|v| (n.clone(), v)))
1164 .collect();
1165 let mut backstops = BTreeMap::new();
1166 for (name, backstop) in &g.backstop.fields {
1167 let (value, truncated, paths) = match backstop {
1168 BackstopField::Text(_) => {
1169 let template = checked.backstops.get(name).ok_or_else(invalid)?;
1170 let rendered = template.render(&values).map_err(|_| invalid())?;
1171 (
1172 TextValue::Text(rendered.text),
1173 rendered.truncated,
1174 rendered.reads.into_iter().collect(),
1175 )
1176 }
1177 BackstopField::List(v) => (TextValue::List(v.clone()), false, vec![]),
1178 };
1179 backstops.insert(
1180 name.clone(),
1181 TextField {
1182 origin: FieldOrigin::Backstop {
1183 reason: TextReason::NotAttempted,
1184 },
1185 value: Some(value),
1186 truncated,
1187 source: TextSource::Fresh,
1188 derivation: Some(Derivation::ExactDerived),
1189 lineage: Some(TextLineage::Backstop { paths }),
1190 },
1191 );
1192 }
1193 let mut context = BTreeMap::new();
1194 for r in &g.carry {
1195 let value = reference(r, &roots)?;
1196 let source = if r.starts_with("bind:") {
1197 match &g.for_each {
1198 ModelForEach::List(f) => match &f.items {
1199 ItemSource::List { list, .. } => list.as_str(),
1200 ItemSource::None => f.over.as_str(),
1201 },
1202 ModelForEach::None => r.as_str(),
1203 }
1204 } else {
1205 r.as_str()
1206 };
1207 let base = crate::expr::base_ref_v2(source);
1208 let l = lineage.get(&base).cloned().unwrap_or_else(|| Lineage {
1209 producers: None,
1210 inputs: base
1211 .strip_prefix("input:")
1212 .map(String::from)
1213 .into_iter()
1214 .collect(),
1215 steps: base
1216 .strip_prefix("step:")
1217 .map(String::from)
1218 .into_iter()
1219 .collect(),
1220 });
1221 let name = r.rsplit(['/', ':']).next().ok_or_else(invalid)?;
1222 context.insert(
1223 name.into(),
1224 ContextValue {
1225 value: value.to_out(),
1226 derivation: Derivation::ExactDerived,
1227 lineage: l,
1228 },
1229 );
1230 }
1231 let projection = g
1232 .projection
1233 .iter()
1234 .map(|r| Ok((r.clone(), reference(r, &roots)?.to_json())))
1235 .collect::<Result<_, Unresolved>>()?;
1236 let Language::Literal(language) = &g.language else {
1237 return Err(invalid());
1238 };
1239 let mut item = Item {
1240 step: id.into(),
1241 declaration: g.clone(),
1242 result: TextItem {
1243 id: instance.clone(),
1244 context,
1245 generated: BTreeMap::new(),
1246 },
1247 backstops,
1248 round: 0,
1249 screens: vec![],
1250 screening: BTreeMap::new(),
1251 pending: Some(GenerationRequest {
1252 owner: id.into(),
1253 screen_projection: None,
1254 backend_id: checked.binding.backend_id.clone(),
1255 document: GenerateRequest {
1256 schema: schema::GENERATE_REQUEST.into(),
1257 step: id.into(),
1258 instance,
1259 template: checked.binding.template.id.clone(),
1260 template_id: checked.binding.template_id.clone(),
1261 instructions,
1262 language: language.clone(),
1263 projection,
1264 output: g.output.clone(),
1265 vocabularies: checked.binding.vocabularies.clone(),
1266 repair: RepairRequest::None,
1267 },
1268 }),
1269 };
1270 if index as u64 >= checked.binding.max_items {
1271 complete(&mut item, TextReason::NotAttempted);
1272 }
1273 items.push(item);
1274 }
1275 Ok(items)
1276}
1277
1278#[cfg(test)]
1279mod tests {
1280 use super::*;
1281
1282 fn fixture() -> (
1283 rustev_contract::plan::Plan,
1284 rustev_contract::descriptor::BackendDescriptor,
1285 ) {
1286 use rustev_contract::definition::{Determinism, Operation};
1287 use rustev_contract::descriptor::{
1288 BackendDescriptor, InputExcess, InputLimit, OperationSupport,
1289 };
1290 let plan = serde_json::from_str(include_str!("testdata/generation.plan.json")).unwrap();
1291 let backend = BackendDescriptor {
1292 ranking: None,
1293 schema: schema::BACKEND.into(),
1294 backend_id: "synthetic-linear-head".into(),
1295 artifact: rustev_contract::ids::ArtifactId::parse(&format!(
1296 "sha256:{}",
1297 "a".repeat(64)
1298 ))
1299 .unwrap(),
1300 operations: vec![OperationSupport {
1301 isolation: None,
1302 screen: None,
1303 operation: Operation::Generate,
1304 output: OutputKind::TextRecord {
1305 structure: TextStructure::JsonSchema,
1306 max_output_bytes: 4096,
1307 languages: vec!["en-GB".into()],
1308 streaming: false,
1309 },
1310 max_options: 32,
1311 }],
1312 input_limit: InputLimit {
1313 max_bytes: 65_536,
1314 on_excess: InputExcess::Refuse,
1315 },
1316 determinism: Determinism::Tolerance,
1317 };
1318 (plan, backend)
1319 }
1320
1321 #[test]
1322 fn ranking_and_duration_proposal_template_reads_are_category_23() {
1323 use crate::{expr::StepTy, value::Ty};
1324 use rustev_contract::definition::{OutcomeDecl, ParamDecl, ParamSource};
1325 for ty in [Ty::Integer, Ty::Ranking { max: 4 }, Ty::Duration] {
1326 for prompt_binding in [false, true] {
1327 let (mut plan, _) = fixture();
1328 let PlanStepDetail::Generate(binding) = &plan.steps[0].detail else {
1329 panic!()
1330 };
1331 let mut prompt = binding.template.clone();
1332 let OutcomeDecl::Propose { params, .. } = &mut plan.definition.policy.rules[0].then
1333 else {
1334 panic!()
1335 };
1336 params.push(ParamDecl {
1337 name: "value".into(),
1338 value: ParamSource::Ref("step:source".into()),
1339 });
1340 let mut source = plan.definition.steps[0].clone();
1341 source.id = "source".into();
1342 let StepBody::Generate(g) = &mut plan.definition.steps[0].body else {
1343 panic!()
1344 };
1345 if prompt_binding {
1346 prompt.params = vec![PromptParam {
1347 name: "value".into(),
1348 ty: PromptParamType::Integer,
1349 }];
1350 prompt.instructions = "Value: {{param.value}}".into();
1351 g.template.params.insert(
1352 "value".into(),
1353 TemplateBinding::Reference("proposal:value".into()),
1354 );
1355 } else {
1356 g.backstop.fields.insert(
1357 "headline".into(),
1358 BackstopField::Text("Value: {{proposal.value}}".into()),
1359 );
1360 }
1361 source.body = StepBody::Exact(rustev_contract::definition::ExactDecl {
1363 op: "count".into(),
1364 version: 1,
1365 args: serde_json::json!({"list":"input:items"}),
1366 });
1367 plan.definition.steps.push(source);
1368 let result = crate::generation::type_check(
1369 &plan.definition,
1370 &BTreeMap::from([("item_text".into(), prompt)]),
1371 &BTreeMap::new(),
1372 &BTreeMap::from([("source".into(), StepTy::Exact(ty.clone()))]),
1373 &BTreeMap::from([("source".into(), Derivation::ExactDerived)]),
1374 &BTreeMap::new(),
1375 );
1376 if ty == Ty::Integer {
1377 assert!(result.is_ok(), "integer control: {:?}", result.err());
1378 } else {
1379 let Err(error) = result else {
1380 panic!("unsupported template read must fail")
1381 };
1382 assert_eq!(error.category, crate::Category::Template);
1383 assert_eq!(error.subject, "step:item_text");
1384 }
1385 }
1386 }
1387 }
1388
1389 #[test]
1390 fn repair_output_budget_covers_only_requested_fields() {
1391 use rustev_contract::scope::Handle;
1392 use rustev_contract::time::Timestamp;
1393 for only_missing in [true, false] {
1394 let (mut plan, backend) = fixture();
1395 let PlanStepDetail::Generate(binding) = &plan.steps[0].detail else {
1396 panic!()
1397 };
1398 let prompt = binding.template.clone();
1399 let StepBody::Generate(g) = &mut plan.definition.steps[0].body else {
1400 panic!()
1401 };
1402 g.repair.only_missing_fields = only_missing;
1403 let compiled = crate::compile_with_templates(
1404 &plan.definition,
1405 &[backend],
1406 &[],
1407 &[],
1408 None,
1409 &[prompt],
1410 )
1411 .unwrap();
1412 let snapshot = rustev_contract::snapshot::Snapshot {
1413 schema: schema::SNAPSHOT.into(),
1414 entries: vec![],
1415 };
1416 let mut stage =
1417 crate::Evaluation::start(&compiled, &snapshot, Timestamp::from_ms(0).unwrap())
1418 .unwrap()
1419 .begin_generation("decision")
1420 .unwrap();
1421 let scope = Scope::tenant_only(
1422 Handle::new("tenant").unwrap(),
1423 Handle::new("revision").unwrap(),
1424 );
1425 let initial = stage.request_document("item_text", &[], 0, &scope).unwrap();
1426 assert!(matches!(
1427 initial.output,
1428 OutputKind::TextRecord {
1429 max_output_bytes: 548,
1430 ..
1431 }
1432 ));
1433 stage
1434 .supply(
1435 "item_text",
1436 &[],
1437 Supplied::Output(RawOutput::TextRecord(BTreeMap::from([
1438 ("headline".into(), TextValue::Text("Keep".into())),
1439 ("why".into(), TextValue::Text(String::new())),
1440 ("tags".into(), TextValue::List(vec!["quiet".into()])),
1441 ]))),
1442 )
1443 .unwrap();
1444 let pending = stage.pending().remove(0);
1445 assert_eq!(pending.document.instance, vec!["repair:1"]);
1446 assert_eq!(
1447 pending.document.output.fields.len(),
1448 if only_missing { 1 } else { 3 }
1449 );
1450 let repair = stage
1451 .request_document("item_text", &pending.document.instance, 0, &scope)
1452 .unwrap();
1453 let OutputKind::TextRecord {
1454 max_output_bytes, ..
1455 } = repair.output
1456 else {
1457 panic!()
1458 };
1459 assert_eq!(max_output_bytes, if only_missing { 400 } else { 548 });
1460 }
1461 }
1462
1463 #[test]
1464 fn evidence_scopes_notices_to_their_generate_step() {
1465 let notices = vec![
1466 TextNotice {
1467 kind: TextNoticeKind::OwnershipRestored,
1468 step: String::new(),
1469 instance: vec![],
1470 field: "judgment".into(),
1471 },
1472 TextNotice {
1473 kind: TextNoticeKind::OwnershipViolation,
1474 step: "first".into(),
1475 instance: vec![],
1476 field: "judgment".into(),
1477 },
1478 ];
1479 for id in ["first", "second"] {
1480 let step = TextStep {
1481 step: id.into(),
1482 state: GenerationState::Ran,
1483 lineage: Lineage::default(),
1484 items: vec![],
1485 text_origin: TextOrigin::None,
1486 counts: TextCounts::default(),
1487 };
1488 let actual = evidence(&step, ¬ices).unwrap();
1489 let expected = if id == "first" {
1490 vec![notices[1].clone()]
1491 } else {
1492 vec![]
1493 };
1494 assert_eq!(actual.notices, expected);
1495 }
1496 }
1497}