1use std::collections::{BTreeMap, BTreeSet, VecDeque};
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::{Arc, Mutex};
6use std::task::{Poll, Waker};
7
8use rustev_contract::definition::StepBody;
9use rustev_contract::descriptor::{BackendDescriptor, OutputKind};
10use rustev_contract::generation::{GenerateRequest, ModelBound, RepairRequest};
11use rustev_contract::generation_result::{FieldOrigin, TextLineage, TextOrigin, TextResult};
12use rustev_contract::judgment::Unresolved;
13use rustev_contract::optimization::{
14 OptimizationAdmission, OptimizationMechanism, OptimizationOutcome,
15};
16use rustev_contract::output::TextValue;
17use rustev_contract::plan::PlanStepDetail;
18use rustev_contract::run::{RequestRecord, RequestResult};
19use rustev_contract::{Document, Identified};
20use rustev_core::evaluate::{SemanticRequest, Supplied};
21use rustev_core::generation_eval::{
22 GenerationError, GenerationEvaluation, GenerationRequest, validate_field,
23};
24use rustev_core::seams::{CancelSignal, GenerationFields};
25
26use crate::driver::{DecisionCtx, RequestOutcome, drive, observation, step_deadline, step_ledger};
27use crate::optimization::{CacheMiss, CacheWrite, KeyParts, RequestKey, request_key};
28use crate::{Inner, Ledger, PreparedPlan};
29
30#[derive(Debug, Clone, PartialEq)]
32pub enum GenerationEvent {
33 Field {
34 step: String,
35 item: Vec<String>,
36 field: String,
37 value: TextValue,
38 attempt_id: String,
39 provisional: bool,
40 },
41 Superseded {
42 step: String,
43 item: Vec<String>,
44 field: String,
45 attempt_id: String,
46 origin: FieldOrigin,
47 },
48 Lagged {
49 count: u64,
50 },
51 Complete {
52 text_origin: TextOrigin,
53 },
54}
55
56struct Queue {
57 events: VecDeque<GenerationEvent>,
58 fields: usize,
59 buffer: usize,
60 dropped: u64,
61 closed: bool,
62 waker: Option<Waker>,
63 accepted: Vec<GenerationEvent>,
64}
65
66pub struct GenerationStream(Arc<Mutex<Queue>>);
68impl GenerationStream {
69 pub async fn recv(&mut self) -> Option<GenerationEvent> {
77 std::future::poll_fn(|cx| {
78 let mut q = self.0.lock().unwrap_or_else(|e| e.into_inner());
79 if let Some(event) = q.events.pop_front() {
80 if matches!(event, GenerationEvent::Field { .. }) {
81 q.fields -= 1;
82 if q.dropped > 0 {
83 let count = std::mem::take(&mut q.dropped);
84 let at = q
85 .events
86 .iter()
87 .position(|e| matches!(e, GenerationEvent::Complete { .. }))
88 .unwrap_or(q.events.len());
89 q.events.insert(at, GenerationEvent::Lagged { count });
90 }
91 }
92 Poll::Ready(Some(event))
93 } else if q.closed {
94 Poll::Ready(None)
95 } else {
96 q.waker = Some(cx.waker().clone());
97 Poll::Pending
98 }
99 })
100 .await
101 }
102}
103
104fn stream_capacity(
107 mut bounds: impl Iterator<Item = (u64, usize)>,
108 buffer: usize,
109) -> Result<(usize, usize), GenerationError> {
110 let overflow = || GenerationError::InvalidState("generation stream capacity overflow".into());
111 let n = bounds.try_fold(0usize, |total, (requests, fields)| {
112 let requests = usize::try_from(requests).map_err(|_| overflow())?;
113 let fields = requests.checked_mul(fields).ok_or_else(overflow)?;
114 total.checked_add(fields).ok_or_else(overflow)
115 })?;
116 let capacity = n
117 .checked_mul(2)
118 .and_then(|n| n.checked_add(buffer))
119 .and_then(|n| n.checked_add(1))
120 .ok_or_else(overflow)?;
121 Ok((n, capacity))
122}
123
124pub(crate) struct Streaming(Arc<Mutex<Queue>>);
125impl Streaming {
126 pub(crate) fn new(
127 plan: &PreparedPlan,
128 buffer: usize,
129 ) -> Result<(Self, GenerationStream), GenerationError> {
130 if !(1..=4096).contains(&buffer) {
131 return Err(GenerationError::InvalidState(
132 "stream_buffer must be in 1..=4096".into(),
133 ));
134 }
135 let bounds = plan.compiled.plan.steps.iter().filter_map(|s| {
136 let PlanStepDetail::Generate(g) = &s.detail else {
137 return None;
138 };
139 Some((g.worst_case_requests, g.output.fields.len()))
140 });
141 let (n, capacity) = stream_capacity(bounds, buffer)?;
142 let queue = Arc::new(Mutex::new(Queue {
143 events: VecDeque::with_capacity(capacity),
144 fields: 0,
145 buffer,
146 dropped: 0,
147 closed: false,
148 waker: None,
149 accepted: Vec::with_capacity(n),
150 }));
151 Ok((Self(queue.clone()), GenerationStream(queue)))
152 }
153 pub(crate) fn finish(&self, text: &TextResult) {
154 let mut q = self.0.lock().unwrap_or_else(|e| e.into_inner());
155 for event in std::mem::take(&mut q.accepted) {
156 if let GenerationEvent::Field {
157 step,
158 item,
159 field,
160 value,
161 attempt_id,
162 ..
163 } = event
164 {
165 let final_field = text
166 .steps
167 .iter()
168 .find(|s| s.step == step)
169 .and_then(|s| s.items.iter().find(|i| i.id == item))
170 .and_then(|i| i.generated.get(&field));
171 if let Some(f) = final_field {
178 let same = f.value.as_ref() == Some(&value)
179 && matches!(&f.lineage,
180 Some(TextLineage::Model { attempt, .. }) if attempt == &attempt_id);
181 if !same {
182 q.events.push_back(GenerationEvent::Superseded {
183 step,
184 item,
185 field,
186 attempt_id,
187 origin: f.origin.clone(),
188 });
189 }
190 }
191 }
192 }
193 q.events.push_back(GenerationEvent::Complete {
194 text_origin: text.text_origin,
195 });
196 let waker = q.waker.take();
197 drop(q);
198 if let Some(w) = waker {
199 w.wake();
200 }
201 }
202}
203impl Drop for Streaming {
204 fn drop(&mut self) {
205 let mut q = self.0.lock().unwrap_or_else(|e| e.into_inner());
206 q.closed = true;
207 let waker = q.waker.take();
208 drop(q);
209 if let Some(w) = waker {
210 w.wake();
211 }
212 }
213}
214
215pub(crate) struct RequestStream {
216 document: GenerateRequest,
217 queue: Arc<Mutex<Queue>>,
218 emitted: Mutex<BTreeSet<String>>,
219}
220pub(crate) struct AttemptFields<'a> {
221 request: &'a RequestStream,
222 attempt: &'a str,
223 pub enabled: bool,
224}
225impl RequestStream {
226 pub(crate) fn attempt<'a>(
227 &'a self,
228 attempt: &'a str,
229 descriptor: &BackendDescriptor,
230 ) -> AttemptFields<'a> {
231 AttemptFields {
232 request: self,
233 attempt,
234 enabled: descriptor.operations.iter().any(|o| {
235 o.operation == rustev_contract::definition::Operation::Generate
236 && matches!(
237 o.output,
238 OutputKind::TextRecord {
239 streaming: true,
240 ..
241 }
242 )
243 }),
244 }
245 }
246}
247impl GenerationFields for AttemptFields<'_> {
248 fn field(&self, name: &str, value: TextValue) {
249 let r = self.request;
250 let Some(field) = r.document.output.fields.iter().find(|f| f.name == name) else {
251 return;
252 };
253 if validate_field(field, Some(&value), &r.document.vocabularies).is_err() {
254 return;
255 }
256 if !r
258 .emitted
259 .lock()
260 .unwrap_or_else(|e| e.into_inner())
261 .insert(name.into())
262 {
263 return;
264 }
265 let event = GenerationEvent::Field {
266 step: r.document.step.clone(),
267 item: item_id(&r.document),
268 field: name.into(),
269 value,
270 attempt_id: self.attempt.into(),
271 provisional: true,
272 };
273 let mut q = r.queue.lock().unwrap_or_else(|e| e.into_inner());
274 if q.fields == q.buffer {
275 q.dropped += 1;
276 return;
277 }
278 q.fields += 1;
279 q.accepted.push(event.clone());
280 q.events.push_back(event);
281 let waker = q.waker.take();
282 drop(q);
283 if let Some(w) = waker {
284 w.wake();
285 }
286 }
287}
288fn item_id(doc: &GenerateRequest) -> Vec<String> {
289 let mut id = doc.instance.clone();
290 if matches!(doc.repair, RepairRequest::Round { .. }) {
291 id.pop();
292 }
293 id
294}
295
296pub(crate) struct StageContext<'a> {
297 pub rt: &'a Inner,
298 pub plan: &'a PreparedPlan,
299 pub deadline: u64,
300 pub submitted: u64,
301 pub cancel: &'a CancelSignal,
302 pub ledger: &'a Ledger,
303 pub decision_id: &'a str,
304 pub principal: &'a [u8],
305 pub evaluation_time: rustev_contract::Timestamp,
306 pub streaming: Option<&'a Streaming>,
307}
308pub(crate) struct PendingWrite {
309 key: RequestKey,
310 write: CacheWrite,
311}
312pub(crate) fn write_cache(cx: &StageContext<'_>, writes: Vec<PendingWrite>) {
313 if let Some(memory) = cx
314 .plan
315 .optimization
316 .as_ref()
317 .and_then(|p| p.memory_cache.as_ref())
318 {
319 for mut w in writes {
320 w.write.now = cx.rt.clock.now();
321 let _ = cx.rt.optimizer.insert(memory, w.key, w.write);
323 }
324 }
325}
326pub(crate) struct ItemClock {
327 start: Mutex<Option<u64>>,
328 duration: u64,
329}
330impl ItemClock {
331 pub(crate) fn start(&self, now: u64) {
332 self.start
333 .lock()
334 .unwrap_or_else(|e| e.into_inner())
335 .get_or_insert(now);
336 }
337 pub(crate) fn deadline(&self, limit: u64) -> u64 {
338 self.start
339 .lock()
340 .unwrap_or_else(|e| e.into_inner())
341 .map_or(limit, |t| limit.min(t.saturating_add(self.duration)))
342 }
343}
344pub(crate) fn pending_write(out: &RequestOutcome, now: u64) -> Option<PendingWrite> {
345 match (&out.supplied, out.record.attempts.last(), &out.cache) {
346 (Some(Supplied::Output(raw)), Some(a), Some((key, generation)))
347 if a.target == 0 && matches!(a.end, rustev_contract::run::AttemptEnd::Output) =>
348 {
349 Some(PendingWrite {
350 key: key.clone(),
351 write: CacheWrite {
352 output: raw.clone(),
353 source_attempt_id: a.attempt_id.clone(),
354 target: 0,
355 captured_generation: *generation,
356 now,
357 },
358 })
359 }
360 _ => None,
361 }
362}
363struct StepBudget {
364 deadline: u64,
365 item_ms: u64,
366 concurrency: usize,
367 ledger: Ledger,
368}
369struct Finished {
370 out: RequestOutcome,
371 source: Option<String>,
372 write: Option<PendingWrite>,
373}
374type Running<'a> = Pin<Box<dyn Future<Output = Result<Finished, GenerationError>> + Send + 'a>>;
375
376pub(crate) async fn run(
378 cx: &StageContext<'_>,
379 stage: &Mutex<GenerationEvaluation<'_>>,
380 mut capture: Option<&mut crate::capture::Capturing<'_>>,
381) -> Result<(Vec<RequestRecord>, Vec<PendingWrite>, bool), GenerationError> {
382 let start = cx.rt.clock.now();
383 let mut budgets = BTreeMap::new();
384 for s in &cx.plan.compiled.plan.definition.steps {
385 if let StepBody::Generate(g) = &s.body {
386 let ModelBound::Literal(item_ms) = g.limits.per_item_deadline_ms else {
387 return Err(GenerationError::InvalidState(
388 "unresolved generation deadline".into(),
389 ));
390 };
391 budgets.insert(
392 s.id.clone(),
393 StepBudget {
394 deadline: step_deadline(
395 cx.deadline,
396 start,
397 cx.plan.compiled.plan.definition.limits.deadline_ms,
398 g.limits.deadline_share,
399 ),
400 item_ms,
401 concurrency: g.limits.concurrency as usize,
402 ledger: step_ledger(&g.limits.max_cost_units),
403 },
404 );
405 }
406 }
407 let mut active: Vec<(String, Vec<String>, Running<'_>)> = vec![];
408 let mut slots: BTreeMap<(String, Vec<String>), Arc<ItemClock>> = BTreeMap::new();
409 let mut records = BTreeMap::new();
410 let mut seen = BTreeMap::new();
411 let mut writes = vec![];
412 loop {
413 if cx.cancel.is_raised() && active.is_empty() {
414 return Ok((records.into_values().collect(), vec![], true));
415 }
416 let pending = stage.lock().unwrap_or_else(|e| e.into_inner()).pending();
417 for req in &pending {
418 let next = seen.len();
419 seen.entry((req.document.step.clone(), req.document.instance.clone()))
420 .or_insert(next);
421 }
422 for req in pending {
424 if cx.cancel.is_raised() || active.len() >= cx.rt.config.max_parallel_requests {
425 break;
426 }
427 let step = req.owner.clone();
428 let item = item_id(&req.document);
429 if active.iter().any(|(s, i, _)| s == &step && i == &item) {
430 continue;
431 }
432 let Some(budget) = budgets.get(&step) else {
433 return Err(GenerationError::InvalidState(
434 "unbound generation step".into(),
435 ));
436 };
437 let occupied = slots.keys().filter(|(s, _)| s == &step).count();
438 if !slots.contains_key(&(step.clone(), item.clone())) && occupied >= budget.concurrency
439 {
440 continue;
441 }
442 let item_clock = slots
443 .entry((step.clone(), item.clone()))
444 .or_insert_with(|| {
445 Arc::new(ItemClock {
446 start: Mutex::new(None),
447 duration: budget.item_ms,
448 })
449 })
450 .clone();
451 active.push((
452 step,
453 item,
454 Box::pin(request(cx, stage, budget, item_clock, req)),
455 ));
456 }
457 if active.is_empty() {
458 break;
459 }
460 let (index, result) = std::future::poll_fn(|task| {
461 for (i, (_, _, f)) in active.iter_mut().enumerate() {
462 if let Poll::Ready(r) = f.as_mut().poll(task) {
463 return Poll::Ready((i, r));
464 }
465 }
466 Poll::Pending
467 })
468 .await;
469 let (step, item, future) = active.remove(index);
470 drop(future);
471 let Finished { out, source, write } = result?;
472 if let Some(supplied) = out.supplied {
473 let attempt = out
474 .record
475 .attempts
476 .last()
477 .map(|a| a.attempt_id.clone())
478 .or_else(|| {
479 out.record
480 .optimization
481 .as_ref()
482 .and_then(|o| o.source_attempt_id.clone())
483 })
484 .unwrap_or_else(|| {
485 format!(
486 "{}/{}/refused",
487 crate::driver::escape(cx.decision_id),
488 crate::driver::escape(&step)
489 )
490 });
491 let mut g = stage.lock().unwrap_or_else(|e| e.into_inner());
492 if let Some(c) = capture.as_deref_mut() {
493 c.record_generation(&g, cx.plan, &out.record, &supplied);
494 }
495 g.supply_attempt(
496 &out.step,
497 &out.instance,
498 supplied,
499 &attempt,
500 (out.record.attempts.len() as u32).max(1),
501 )?;
502 if let Some(source) = source {
503 g.note_cached_attempt(&attempt, &source);
504 }
505 if !g
506 .pending()
507 .iter()
508 .any(|r| r.owner == step && item_id(&r.document) == item)
509 {
510 slots.remove(&(step, item));
511 }
512 }
513 if let Some(index) = seen.get(&(out.record.step.clone(), out.record.instance.clone())) {
514 records.insert(*index, out.record);
515 }
516 if let Some(write) = write {
517 writes.push(write);
518 }
519 }
520 Ok((
521 records.into_values().collect(),
522 writes,
523 cx.cancel.is_raised(),
524 ))
525}
526
527async fn request(
528 cx: &StageContext<'_>,
529 stage: &Mutex<GenerationEvaluation<'_>>,
530 budget: &StepBudget,
531 item_clock: Arc<ItemClock>,
532 req: GenerationRequest,
533) -> Result<Finished, GenerationError> {
534 let deadline = budget.deadline;
535 let is_screen = req.screen_projection.is_some();
536 let projection = match req.screen_projection {
537 Some(bytes) => bytes,
538 None => req
539 .document
540 .canonical()
541 .map_err(|e| GenerationError::InvalidState(e.to_string()))?,
542 };
543 let semantic = SemanticRequest {
544 step: req.document.step.clone(),
545 instance: req.document.instance.clone(),
546 backend_id: req.backend_id,
547 projection,
548 };
549 let stream = cx.streaming.filter(|_| !is_screen).map(|s| RequestStream {
550 document: req.document,
551 queue: s.0.clone(),
552 emitted: Mutex::new(BTreeSet::new()),
553 });
554 let drive_cx = DecisionCtx {
555 rt: cx.rt,
556 observing: None,
557 plan: cx.plan,
558 deadline,
559 submitted: cx.submitted,
560 cancel: cx.cancel,
561 ledger: cx.ledger,
562 decision_id: cx.decision_id,
563 principal: cx.principal,
564 evaluation_time: cx.evaluation_time,
565 ev: None,
566 generation: Some(stage),
567 step_ledger: Some(&budget.ledger),
568 stream: stream.as_ref(),
569 item_clock: Some(&item_clock),
570 deferred_cache: None,
573 live: None,
574 };
575 let mut cache = None;
576 let mut diagnostics = vec![];
577 if !is_screen
578 && !cx.cancel.is_raised()
579 && cx.rt.clock.now() < item_clock.deadline(deadline)
580 && let Some(policy) = &cx.plan.optimization
581 && policy.memory_cache.is_some()
582 && let Some(target) = cx
583 .plan
584 .steps
585 .get(&semantic.step)
586 .and_then(|s| s.targets.first())
587 && let Ok(descriptor) = target.registered.backend.descriptor().id()
588 {
589 let key = request_key(KeyParts {
590 plan_id: &cx.plan.compiled.id,
591 policy,
592 principal: cx.principal,
593 backend_artifact: &target.artifact,
594 backend_descriptor: &descriptor,
595 step: &semantic.step,
596 instance: &semantic.instance,
597 projection: &semantic.projection,
598 });
599 let generation = cx.rt.optimizer.generation(&key.namespace);
600 let lookup = cx.rt.optimizer.lookup(&key, cx.rt.clock.now());
601 if let Ok(hit) = lookup {
602 item_clock.start(cx.rt.clock.now());
603 let mut o = observation(
604 policy,
605 OptimizationMechanism::MemoryCache,
606 OptimizationOutcome::Hit,
607 hit.generation,
608 vec![],
609 );
610 o.entry_id = Some(hit.entry_id);
611 o.age_ms = hit.age_ms;
612 o.source_attempt_id = Some(hit.source_attempt_id.clone());
613 let source = hit
614 .source_attempt_id
615 .split('/')
616 .next()
617 .map(crate::driver::unescape)
618 .unwrap_or_default();
619 let record = RequestRecord {
620 screen: None,
621 step: semantic.step.clone(),
622 instance: semantic.instance.clone(),
623 result: RequestResult::Output { target: hit.target },
624 attempts: vec![],
625 transitions: vec![],
626 optimization: Some(o),
627 };
628 return Ok(Finished {
629 out: RequestOutcome {
630 step: semantic.step,
631 instance: semantic.instance,
632 supplied: Some(Supplied::Output(hit.output)),
633 record,
634 cache: None,
635 },
636 source: Some(source),
637 write: None,
638 });
639 }
640 if let Err(miss) = lookup {
641 let detail = crate::driver::cache_miss_detail(miss);
642 if miss == CacheMiss::Collision && policy.admission == OptimizationAdmission::Refuse {
643 item_clock.start(cx.rt.clock.now());
644 let reason = Unresolved::BackendUnavailable {
645 detail: detail.into(),
646 };
647 let record = RequestRecord {
648 screen: None,
649 step: semantic.step.clone(),
650 instance: semantic.instance.clone(),
651 result: RequestResult::Failed(reason.clone()),
652 attempts: vec![],
653 transitions: vec![],
654 optimization: Some(observation(
655 policy,
656 OptimizationMechanism::MemoryCache,
657 OptimizationOutcome::Refused,
658 generation,
659 vec![detail.into()],
660 )),
661 };
662 return Ok(Finished {
663 out: RequestOutcome {
664 step: semantic.step,
665 instance: semantic.instance,
666 supplied: Some(Supplied::Failed(reason)),
667 record,
668 cache: None,
669 },
670 source: None,
671 write: None,
672 });
673 }
674 diagnostics.push(detail.into());
675 }
676 cache = Some((key, generation));
677 }
678 let mut out = drive(
679 &drive_cx,
680 semantic,
681 cache.as_ref().map(|(k, _)| k.clone()),
682 cache.as_ref().map_or(0, |(_, g)| *g),
683 diagnostics,
684 )
685 .await;
686 crate::driver::record_cache_cost(&mut out);
691 let write = if is_screen {
692 None
693 } else {
694 pending_write(&out, cx.rt.clock.now())
695 };
696 Ok(Finished {
697 out,
698 source: None,
699 write,
700 })
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706
707 #[test]
708 fn stream_capacity_rejects_overflow_before_allocation() {
709 struct Case {
710 label: &'static str,
711 bounds: Vec<(u64, usize)>,
712 buffer: usize,
713 }
714
715 let max = usize::MAX as u64;
716 for Case {
717 label,
718 bounds,
719 buffer,
720 } in [
721 Case {
722 label: "Per-step multiplication",
723 bounds: vec![(max, 2)],
724 buffer: 1,
725 },
726 Case {
727 label: "Sum across steps",
728 bounds: vec![(max, 1), (1, 1)],
729 buffer: 1,
730 },
731 Case {
732 label: "Control-event multiplication",
733 bounds: vec![(max, 1)],
734 buffer: 1,
735 },
736 Case {
737 label: "Field-buffer addition",
738 bounds: vec![(max / 2, 1)],
739 buffer: 2,
741 },
742 Case {
743 label: "Terminal-event addition",
744 bounds: vec![(max / 2, 1)],
745 buffer: 1,
747 },
748 Case {
749 label: "Conversion on narrower targets",
750 bounds: vec![(u64::MAX, 2)],
751 buffer: 1,
752 },
753 ] {
754 assert!(
755 matches!(
756 stream_capacity(bounds.into_iter(), buffer),
757 Err(GenerationError::InvalidState(detail))
758 if detail == "generation stream capacity overflow"
759 ),
760 "{label} (buffer = {buffer}) must reject capacity overflow"
761 );
762 }
763 }
764
765 #[test]
766 fn stream_capacity_reserves_control_events_separately() {
767 assert_eq!(
768 stream_capacity([(2, 3), (4, 2)].into_iter(), 1).unwrap(),
769 (14, 30)
770 );
771 assert_eq!(
772 stream_capacity(std::iter::empty(), 4096).unwrap(),
773 (0, 4097)
774 );
775 }
776}