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