1use std::pin::Pin;
2use std::task::{Context, Poll};
3
4use crate::support::*;
5use futures_util::Stream;
6
7pub use lash_core::{AssistantOutput, TurnIssue};
8
9#[derive(Clone, Copy, Default)]
16pub(crate) struct TurnSinks<'a> {
17 events: Option<&'a dyn EventSink>,
18 turn_events: Option<&'a dyn TurnActivitySink>,
19}
20
21impl<'a> TurnSinks<'a> {
22 pub(crate) fn turn(events: &'a dyn TurnActivitySink) -> Self {
23 Self {
24 events: None,
25 turn_events: Some(events),
26 }
27 }
28
29 pub(crate) fn session(events: &'a dyn EventSink) -> Self {
30 Self {
31 events: Some(events),
32 turn_events: None,
33 }
34 }
35
36 fn events(&self) -> Option<&'a dyn EventSink> {
37 self.events
38 }
39
40 fn turn_events(&self) -> Option<&'a dyn TurnActivitySink> {
41 self.turn_events
42 }
43}
44
45#[derive(Clone, Default)]
50pub(crate) struct TurnCancelRegistry {
51 inner: Arc<StdMutex<TurnCancelRegistryInner>>,
52}
53
54#[derive(Default)]
55struct TurnCancelRegistryInner {
56 next_id: u64,
57 active: BTreeMap<u64, CancellationToken>,
58}
59
60impl TurnCancelRegistry {
61 fn register(&self, token: CancellationToken) -> TurnCancelGuard {
64 let mut inner = self.inner.lock().expect("turn cancel registry");
65 let id = inner.next_id;
66 inner.next_id += 1;
67 inner.active.insert(id, token);
68 TurnCancelGuard {
69 registry: Arc::clone(&self.inner),
70 id,
71 }
72 }
73
74 pub(crate) fn cancel_all(&self) -> usize {
75 let inner = self.inner.lock().expect("turn cancel registry");
76 for token in inner.active.values() {
77 token.cancel();
78 }
79 inner.active.len()
80 }
81}
82
83pub(crate) struct TurnCancelGuard {
84 registry: Arc<StdMutex<TurnCancelRegistryInner>>,
85 id: u64,
86}
87
88impl Drop for TurnCancelGuard {
89 fn drop(&mut self) {
90 self.registry
91 .lock()
92 .expect("turn cancel registry")
93 .active
94 .remove(&self.id);
95 }
96}
97
98pub struct TurnBuilder {
99 pub(crate) runtime: RuntimeHandle,
100 pub(crate) effect_host: Arc<dyn EffectHost>,
101 pub(crate) active_plugins: Vec<ActivePluginBinding>,
102 pub(crate) input: TurnInput,
103 pub(crate) cancel: CancellationToken,
104 pub(crate) cancels: TurnCancelRegistry,
105 pub(crate) protocol_turn_options: Option<ProtocolTurnOptions>,
106 pub(crate) provider: Option<ProviderHandle>,
107 pub(crate) turn_id: Option<String>,
108}
109
110impl TurnBuilder {
111 pub fn cancel(mut self, cancel: CancellationToken) -> Self {
112 self.cancel = cancel;
113 self
114 }
115
116 pub fn protocol_turn_options(mut self, options: ProtocolTurnOptions) -> Self {
117 self.protocol_turn_options = Some(options);
118 self
119 }
120
121 pub fn provider(mut self, provider: ProviderHandle) -> Self {
122 self.provider = Some(provider);
123 self
124 }
125
126 pub fn turn_id(mut self, id: impl Into<String>) -> Self {
127 self.turn_id = Some(id.into());
128 self
129 }
130
131 pub fn prompt_template(mut self, template: PromptTemplate) -> Self {
132 self.input.turn_context.set_prompt_template(template);
133 self
134 }
135
136 pub fn prompt_contribution(mut self, contribution: PromptContribution) -> Self {
137 self.input
138 .turn_context
139 .add_prompt_contribution(contribution);
140 self
141 }
142
143 pub fn replace_prompt_slot(
144 mut self,
145 slot: PromptSlot,
146 contributions: impl IntoIterator<Item = PromptContribution>,
147 ) -> Self {
148 self.input
149 .turn_context
150 .replace_prompt_slot(slot, contributions);
151 self
152 }
153
154 pub fn clear_prompt_slot(mut self, slot: PromptSlot) -> Self {
155 self.input.turn_context.clear_prompt_slot(slot);
156 self
157 }
158
159 pub fn prompt_layer(mut self, layer: PromptLayer) -> Self {
160 self.input.turn_context.set_prompt_layer(layer);
161 self
162 }
163
164 pub fn with_plugin_input<P: PluginBinding>(mut self, input: P::Input) -> Self {
170 self.input.turn_context.insert_plugin_input(P::ID, input);
171 self
172 }
173
174 pub fn effects(self, controller: &dyn RuntimeEffectController) -> ScopedTurnBuilder<'_> {
175 ScopedTurnBuilder {
176 builder: self,
177 controller,
178 }
179 }
180
181 pub async fn run(self) -> Result<TurnOutput> {
182 let collector = RunActivityCollector::default();
183 let result = self.stream_to(&collector).await?;
184 Ok(TurnOutput {
185 result,
186 activities: collector.into_activities(),
187 })
188 }
189
190 pub async fn stream_to(self, events: &dyn TurnActivitySink) -> Result<TurnResult> {
191 let effect_host = Arc::clone(&self.effect_host);
192 reject_configured_durable_effect_host(effect_host.as_ref(), "turn")?;
193 self.stream_to_with_effect_host(events, effect_host.as_ref())
194 .await
195 }
196
197 pub fn stream(self) -> Result<TurnStream> {
198 let effect_host = Arc::clone(&self.effect_host);
199 reject_configured_durable_effect_host(effect_host.as_ref(), "turn stream")?;
200 self.stream_with_effect_host(effect_host.as_ref())
201 }
202
203 pub fn advanced(self) -> AdvancedTurn {
206 AdvancedTurn { builder: self }
207 }
208
209 fn resolved_turn_id(&self) -> String {
210 self.turn_id
211 .clone()
212 .or_else(|| self.input.trace_turn_id.clone())
213 .unwrap_or_else(fresh_turn_id)
214 }
215
216 fn turn_scope(&self, turn_id: &str) -> lash_core::ExecutionScope {
217 lash_core::ExecutionScope::turn(self.runtime.observe().session_id(), turn_id)
218 }
219
220 pub(crate) fn prepare(
221 mut self,
222 trace_turn_id: Option<String>,
223 ) -> Result<(RuntimeHandle, TurnInput, CancellationToken, TurnCancelGuard)> {
224 if let Some(options) = self.protocol_turn_options {
225 self.input.protocol_turn_options = Some(options);
226 }
227 if let Some(provider) = self.provider {
228 self.input.turn_context.set_provider(provider);
229 }
230 if let Some(trace_turn_id) = trace_turn_id {
231 self.input.trace_turn_id = Some(trace_turn_id);
232 }
233 validate_required_plugin_inputs(&self.active_plugins, &self.input)?;
234 let cancel_guard = self.cancels.register(self.cancel.clone());
235 Ok((self.runtime, self.input, self.cancel, cancel_guard))
236 }
237
238 async fn stream_to_with_effect_host(
239 self,
240 events: &dyn TurnActivitySink,
241 effect_host: &dyn EffectHost,
242 ) -> Result<TurnResult> {
243 let turn_id = self.resolved_turn_id();
244 let scoped_effect_controller = effect_host.scoped(self.turn_scope(&turn_id))?;
245 self.stream_to_with_scope(events, scoped_effect_controller, Some(turn_id))
246 .await
247 }
248
249 async fn stream_to_with_effect_controller(
250 self,
251 events: &dyn TurnActivitySink,
252 controller: &dyn RuntimeEffectController,
253 ) -> Result<TurnResult> {
254 let turn_id = self.resolved_turn_id();
255 let scoped_effect_controller =
256 ScopedEffectController::borrowed(controller, self.turn_scope(&turn_id))?;
257 self.stream_to_with_scope(events, scoped_effect_controller, Some(turn_id))
258 .await
259 }
260
261 async fn stream_to_with_scope(
262 self,
263 events: &dyn TurnActivitySink,
264 scoped_effect_controller: ScopedEffectController<'_>,
265 trace_turn_id: Option<String>,
266 ) -> Result<TurnResult> {
267 let (runtime, input, cancel, _cancel_guard) = self.prepare(trace_turn_id)?;
268 stream_prepared_turn(
269 &runtime,
270 input,
271 TurnSinks::turn(events),
272 scoped_effect_controller,
273 cancel,
274 )
275 .await
276 }
277
278 fn stream_with_effect_host(self, effect_host: &dyn EffectHost) -> Result<TurnStream> {
279 let turn_id = self.resolved_turn_id();
280 let scoped_effect_controller = effect_host
281 .scoped_static(self.turn_scope(&turn_id))?
282 .ok_or(EmbedError::StaticTurnStreamRequiresStaticEffectHost)?;
283 self.stream_with_scope(scoped_effect_controller, Some(turn_id))
284 }
285
286 fn stream_with_scope(
287 self,
288 scoped_effect_controller: ScopedEffectController<'static>,
289 trace_turn_id: Option<String>,
290 ) -> Result<TurnStream> {
291 let (runtime, input, cancel, cancel_guard) = self.prepare(trace_turn_id)?;
292 let (tx, rx) = mpsc::channel(64);
293 let sink = ChannelTurnActivitySink { tx };
294 let completion = tokio::spawn(async move {
295 let _cancel_guard = cancel_guard;
296 stream_prepared_turn(
297 &runtime,
298 input,
299 TurnSinks::turn(&sink),
300 scoped_effect_controller,
301 cancel,
302 )
303 .await
304 });
305 Ok(TurnStream {
306 activities: rx,
307 completion,
308 })
309 }
310}
311
312pub struct ScopedTurnBuilder<'run> {
313 builder: TurnBuilder,
314 controller: &'run dyn RuntimeEffectController,
315}
316
317impl<'run> ScopedTurnBuilder<'run> {
318 pub fn cancel(mut self, cancel: CancellationToken) -> Self {
319 self.builder = self.builder.cancel(cancel);
320 self
321 }
322
323 pub fn protocol_turn_options(mut self, options: ProtocolTurnOptions) -> Self {
324 self.builder = self.builder.protocol_turn_options(options);
325 self
326 }
327
328 pub fn provider(mut self, provider: ProviderHandle) -> Self {
329 self.builder = self.builder.provider(provider);
330 self
331 }
332
333 pub fn turn_id(mut self, id: impl Into<String>) -> Self {
334 self.builder = self.builder.turn_id(id);
335 self
336 }
337
338 pub fn prompt_template(mut self, template: PromptTemplate) -> Self {
339 self.builder = self.builder.prompt_template(template);
340 self
341 }
342
343 pub fn prompt_contribution(mut self, contribution: PromptContribution) -> Self {
344 self.builder = self.builder.prompt_contribution(contribution);
345 self
346 }
347
348 pub fn replace_prompt_slot(
349 mut self,
350 slot: PromptSlot,
351 contributions: impl IntoIterator<Item = PromptContribution>,
352 ) -> Self {
353 self.builder = self.builder.replace_prompt_slot(slot, contributions);
354 self
355 }
356
357 pub fn clear_prompt_slot(mut self, slot: PromptSlot) -> Self {
358 self.builder = self.builder.clear_prompt_slot(slot);
359 self
360 }
361
362 pub fn prompt_layer(mut self, layer: PromptLayer) -> Self {
363 self.builder = self.builder.prompt_layer(layer);
364 self
365 }
366
367 pub fn with_plugin_input<P: PluginBinding>(mut self, input: P::Input) -> Self {
368 self.builder = self.builder.with_plugin_input::<P>(input);
369 self
370 }
371
372 pub async fn run(self) -> Result<TurnOutput> {
373 let collector = RunActivityCollector::default();
374 let result = self.stream_to(&collector).await?;
375 Ok(TurnOutput {
376 result,
377 activities: collector.into_activities(),
378 })
379 }
380
381 pub async fn stream_to(self, events: &dyn TurnActivitySink) -> Result<TurnResult> {
382 self.builder
383 .stream_to_with_effect_controller(events, self.controller)
384 .await
385 }
386}
387
388pub struct AdvancedTurn {
395 builder: TurnBuilder,
396}
397
398impl AdvancedTurn {
399 pub async fn run_with_scope(
400 self,
401 scoped_effect_controller: ScopedEffectController<'_>,
402 ) -> Result<TurnOutput> {
403 let collector = RunActivityCollector::default();
404 let result = self
405 .stream_to_with_scope(&collector, scoped_effect_controller)
406 .await?;
407 Ok(TurnOutput {
408 result,
409 activities: collector.into_activities(),
410 })
411 }
412
413 pub async fn collect_with_scope(
414 self,
415 events: &dyn TurnActivitySink,
416 scoped_effect_controller: ScopedEffectController<'_>,
417 ) -> Result<TurnOutput> {
418 let collector = RunActivityCollector::default();
419 let fanout = BorrowedTurnActivityFanout {
420 live: events,
421 collector: &collector,
422 };
423 let result = self
424 .stream_to_with_scope(&fanout, scoped_effect_controller)
425 .await?;
426 Ok(TurnOutput {
427 result,
428 activities: collector.into_activities(),
429 })
430 }
431
432 pub async fn stream_to_with_scope(
433 self,
434 events: &dyn TurnActivitySink,
435 scoped_effect_controller: ScopedEffectController<'_>,
436 ) -> Result<TurnResult> {
437 let trace_turn_id = trace_turn_id_for_scope(&self.builder, &scoped_effect_controller);
438 self.builder
439 .stream_to_with_scope(events, scoped_effect_controller, trace_turn_id)
440 .await
441 }
442
443 pub fn stream_with_scope(
444 self,
445 scoped_effect_controller: ScopedEffectController<'static>,
446 ) -> Result<TurnStream> {
447 let trace_turn_id = trace_turn_id_for_scope(&self.builder, &scoped_effect_controller);
448 self.builder
449 .stream_with_scope(scoped_effect_controller, trace_turn_id)
450 }
451
452 pub async fn collect_session_events_with_scope(
454 self,
455 events: &dyn EventSink,
456 scoped_effect_controller: ScopedEffectController<'_>,
457 ) -> Result<TurnResult> {
458 let trace_turn_id = trace_turn_id_for_scope(&self.builder, &scoped_effect_controller);
459 let (runtime, input, cancel, _cancel_guard) = self.builder.prepare(trace_turn_id)?;
460 stream_prepared_turn(
461 &runtime,
462 input,
463 TurnSinks::session(events),
464 scoped_effect_controller,
465 cancel,
466 )
467 .await
468 }
469}
470
471pub struct TurnStream {
472 activities: mpsc::Receiver<Result<TurnActivity>>,
473 completion: JoinHandle<Result<TurnResult>>,
474}
475
476impl TurnStream {
477 pub async fn next_activity(&mut self) -> Option<Result<TurnActivity>> {
478 self.activities.recv().await
479 }
480
481 pub async fn finish(self) -> Result<TurnResult> {
482 self.completion.await.map_err(|err| {
483 EmbedError::Runtime(lash_core::RuntimeError::new(
484 RuntimeErrorCode::TurnStreamJoin,
485 format!("turn stream task failed: {err}"),
486 ))
487 })?
488 }
489}
490
491impl Stream for TurnStream {
492 type Item = Result<TurnActivity>;
493
494 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
495 self.activities.poll_recv(cx)
496 }
497}
498
499pub struct QueuedTurnBuilder {
500 pub(crate) runtime: RuntimeHandle,
501 pub(crate) effect_host: Arc<dyn EffectHost>,
502 pub(crate) cancel: CancellationToken,
503 pub(crate) cancels: TurnCancelRegistry,
504 pub(crate) batch_ids: Vec<String>,
505 pub(crate) drain_id: Option<String>,
506}
507
508impl QueuedTurnBuilder {
509 pub fn cancel(mut self, cancel: CancellationToken) -> Self {
510 self.cancel = cancel;
511 self
512 }
513
514 pub fn batch_ids(mut self, batch_ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
515 self.batch_ids = batch_ids.into_iter().map(Into::into).collect();
516 self
517 }
518
519 pub fn drain_id(mut self, drain_id: impl Into<String>) -> Self {
520 self.drain_id = Some(drain_id.into());
521 self
522 }
523
524 pub fn effects(self, controller: &dyn RuntimeEffectController) -> ScopedQueuedTurnBuilder<'_> {
525 ScopedQueuedTurnBuilder {
526 builder: self,
527 controller,
528 }
529 }
530
531 pub async fn run(self) -> Result<Option<TurnOutput>> {
532 let collector = RunActivityCollector::default();
533 let Some(result) = self.stream_to(&collector).await? else {
534 return Ok(None);
535 };
536 Ok(Some(TurnOutput {
537 result,
538 activities: collector.into_activities(),
539 }))
540 }
541
542 pub async fn stream_to(self, events: &dyn TurnActivitySink) -> Result<Option<TurnResult>> {
543 let effect_host = Arc::clone(&self.effect_host);
544 reject_configured_durable_effect_host(effect_host.as_ref(), "queued turn")?;
545 self.stream_to_with_effect_host(events, effect_host.as_ref())
546 .await
547 }
548
549 pub fn advanced(self) -> AdvancedQueuedTurn {
550 AdvancedQueuedTurn { builder: self }
551 }
552
553 fn resolved_drain_id(&self) -> String {
554 self.drain_id
555 .clone()
556 .or_else(|| self.batch_ids.first().cloned())
557 .unwrap_or_else(fresh_queue_drain_id)
558 }
559
560 async fn stream_to_with_effect_host(
561 self,
562 events: &dyn TurnActivitySink,
563 effect_host: &dyn EffectHost,
564 ) -> Result<Option<TurnResult>> {
565 let drain_id = self.resolved_drain_id();
566 let scope =
567 lash_core::ExecutionScope::queue_drain(self.runtime.observe().session_id(), drain_id);
568 let scoped_effect_controller = effect_host.scoped(scope)?;
569 self.stream_to_with_scope(events, scoped_effect_controller)
570 .await
571 }
572
573 async fn stream_to_with_effect_controller(
574 self,
575 events: &dyn TurnActivitySink,
576 controller: &dyn RuntimeEffectController,
577 ) -> Result<Option<TurnResult>> {
578 let drain_id = self.resolved_drain_id();
579 let scope =
580 lash_core::ExecutionScope::queue_drain(self.runtime.observe().session_id(), drain_id);
581 let scoped_effect_controller = ScopedEffectController::borrowed(controller, scope)?;
582 self.stream_to_with_scope(events, scoped_effect_controller)
583 .await
584 }
585
586 async fn stream_to_with_scope(
587 self,
588 events: &dyn TurnActivitySink,
589 scoped_effect_controller: ScopedEffectController<'_>,
590 ) -> Result<Option<TurnResult>> {
591 let Self {
592 runtime,
593 effect_host: _,
594 cancel,
595 cancels,
596 batch_ids,
597 drain_id: _,
598 } = self;
599 let _cancel_guard = cancels.register(cancel.clone());
600 stream_next_queued_prepared_turn(
601 &runtime,
602 TurnSinks::turn(events),
603 scoped_effect_controller,
604 cancel,
605 &batch_ids,
606 )
607 .await
608 }
609}
610
611pub struct ScopedQueuedTurnBuilder<'run> {
612 builder: QueuedTurnBuilder,
613 controller: &'run dyn RuntimeEffectController,
614}
615
616impl<'run> ScopedQueuedTurnBuilder<'run> {
617 pub fn cancel(mut self, cancel: CancellationToken) -> Self {
618 self.builder = self.builder.cancel(cancel);
619 self
620 }
621
622 pub fn batch_ids(mut self, batch_ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
623 self.builder = self.builder.batch_ids(batch_ids);
624 self
625 }
626
627 pub fn drain_id(mut self, drain_id: impl Into<String>) -> Self {
628 self.builder = self.builder.drain_id(drain_id);
629 self
630 }
631
632 pub async fn run(self) -> Result<Option<TurnOutput>> {
633 let collector = RunActivityCollector::default();
634 let Some(result) = self.stream_to(&collector).await? else {
635 return Ok(None);
636 };
637 Ok(Some(TurnOutput {
638 result,
639 activities: collector.into_activities(),
640 }))
641 }
642
643 pub async fn stream_to(self, events: &dyn TurnActivitySink) -> Result<Option<TurnResult>> {
644 self.builder
645 .stream_to_with_effect_controller(events, self.controller)
646 .await
647 }
648}
649
650pub struct AdvancedQueuedTurn {
651 builder: QueuedTurnBuilder,
652}
653
654impl AdvancedQueuedTurn {
655 pub async fn stream_to_with_scope(
656 self,
657 events: &dyn TurnActivitySink,
658 scoped_effect_controller: ScopedEffectController<'_>,
659 ) -> Result<Option<TurnResult>> {
660 self.builder
661 .stream_to_with_scope(events, scoped_effect_controller)
662 .await
663 }
664}
665
666fn fresh_turn_id() -> String {
667 lash_core::TurnActivityId::fresh().0
668}
669
670fn fresh_queue_drain_id() -> String {
671 format!("queue-drain:{}", fresh_turn_id())
672}
673
674fn trace_turn_id_for_scope(
675 builder: &TurnBuilder,
676 scoped_effect_controller: &ScopedEffectController<'_>,
677) -> Option<String> {
678 if scoped_effect_controller
679 .execution_scope()
680 .validates_turn_trace_id()
681 {
682 Some(
683 builder
684 .turn_id
685 .clone()
686 .unwrap_or_else(|| scoped_effect_controller.scope_id().to_string()),
687 )
688 } else {
689 builder
690 .turn_id
691 .clone()
692 .or_else(|| builder.input.trace_turn_id.clone())
693 }
694}
695
696fn reject_configured_durable_effect_host(
697 effect_host: &dyn EffectHost,
698 operation: &'static str,
699) -> Result<()> {
700 if effect_host.durability_tier() == DurabilityTier::Durable {
701 return Err(EmbedError::DurableEffectHostRequiresHandlerContext { operation });
702 }
703 Ok(())
704}
705
706pub(crate) async fn stream_next_queued_prepared_turn(
707 runtime: &RuntimeHandle,
708 sinks: TurnSinks<'_>,
709 scoped_effect_controller: ScopedEffectController<'_>,
710 cancel: CancellationToken,
711 batch_ids: &[String],
712) -> Result<Option<TurnResult>> {
713 let turn = Box::pin(stream_next_queued_prepared_assembled(
714 runtime,
715 sinks,
716 scoped_effect_controller,
717 cancel,
718 batch_ids,
719 ))
720 .await?;
721 Ok(turn.map(TurnResult::from_assembled))
722}
723
724pub(crate) async fn stream_next_queued_prepared_assembled(
725 runtime: &RuntimeHandle,
726 sinks: TurnSinks<'_>,
727 scoped_effect_controller: ScopedEffectController<'_>,
728 cancel: CancellationToken,
729 batch_ids: &[String],
730) -> Result<Option<AssembledTurn>> {
731 let writer_handle = runtime.writer();
732 let mut writer = writer_handle.lock().await;
733 let observation_sink = SessionObservationTurnActivitySink {
734 runtime: runtime.clone(),
735 live: sinks.turn_events(),
736 };
737 let opts = turn_options(
738 sinks.events(),
739 &observation_sink,
740 scoped_effect_controller,
741 cancel,
742 );
743 let turn = if batch_ids.is_empty() {
744 writer.stream_next_queued_work(opts).await?
745 } else {
746 writer.stream_selected_queued_work(opts, batch_ids).await?
747 };
748 runtime.publish_from(&writer);
749 Ok(turn)
750}
751
752fn turn_options<'a>(
753 events: Option<&'a dyn EventSink>,
754 turn_events: &'a dyn TurnActivitySink,
755 scoped_effect_controller: ScopedEffectController<'a>,
756 cancel: CancellationToken,
757) -> lash_core::TurnOptions<'a> {
758 let mut opts = lash_core::TurnOptions::new(cancel, scoped_effect_controller);
759 if let Some(events) = events {
760 opts = opts.with_events(events);
761 }
762 opts.with_turn_events(turn_events)
763}
764
765struct SessionObservationTurnActivitySink<'a> {
766 runtime: RuntimeHandle,
767 live: Option<&'a dyn TurnActivitySink>,
768}
769
770#[async_trait]
771impl TurnActivitySink for SessionObservationTurnActivitySink<'_> {
772 fn is_noop(&self) -> bool {
773 false
774 }
775
776 async fn emit(&self, activity: TurnActivity) {
777 self.runtime.record_turn_activity(activity.clone());
778 if let Some(live) = self.live {
779 live.emit(activity).await;
780 }
781 }
782}
783
784struct ChannelTurnActivitySink {
785 tx: mpsc::Sender<Result<TurnActivity>>,
786}
787
788#[async_trait]
789impl TurnActivitySink for ChannelTurnActivitySink {
790 async fn emit(&self, activity: TurnActivity) {
791 let _ = self.tx.send(Ok(activity)).await;
792 }
793}
794fn validate_required_plugin_inputs(
795 active_plugins: &[ActivePluginBinding],
796 input: &TurnInput,
797) -> Result<()> {
798 for plugin in active_plugins {
799 if plugin.requires_turn_input && !input.turn_context.has_plugin_input(plugin.id) {
800 return Err(EmbedError::MissingPluginTurnInput {
801 plugin_id: plugin.id,
802 });
803 }
804 }
805 Ok(())
806}
807
808pub(crate) async fn stream_prepared_turn(
809 runtime: &RuntimeHandle,
810 input: TurnInput,
811 sinks: TurnSinks<'_>,
812 scoped_effect_controller: ScopedEffectController<'_>,
813 cancel: CancellationToken,
814) -> Result<TurnResult> {
815 let turn = Box::pin(stream_prepared_assembled(
816 runtime,
817 input,
818 sinks,
819 scoped_effect_controller,
820 cancel,
821 ))
822 .await?;
823 Ok(TurnResult::from_assembled(turn))
824}
825
826pub(crate) async fn stream_prepared_assembled(
827 runtime: &RuntimeHandle,
828 input: TurnInput,
829 sinks: TurnSinks<'_>,
830 scoped_effect_controller: ScopedEffectController<'_>,
831 cancel: CancellationToken,
832) -> Result<AssembledTurn> {
833 let turn = Box::pin(stream_prepared_agent_frame_run(
834 runtime,
835 input,
836 sinks,
837 scoped_effect_controller,
838 cancel,
839 ))
840 .await?;
841 turn.into_final_turn().ok_or_else(|| {
842 EmbedError::Runtime(lash_core::RuntimeError::new(
843 RuntimeErrorCode::EmptyAgentFrameRun,
844 "runtime completed without an assembled turn",
845 ))
846 })
847}
848
849pub(crate) async fn stream_prepared_agent_frame_run(
850 runtime: &RuntimeHandle,
851 input: TurnInput,
852 sinks: TurnSinks<'_>,
853 scoped_effect_controller: ScopedEffectController<'_>,
854 cancel: CancellationToken,
855) -> Result<lash_core::AgentFrameRun> {
856 let writer_handle = runtime.writer();
857 let mut writer = writer_handle.lock().await;
858 if let Some(extension) = input.protocol_extension.as_ref() {
859 writer
860 .validate_protocol_turn_extension(extension)
861 .await
862 .map_err(EmbedError::Session)?;
863 }
864 let observation_sink = SessionObservationTurnActivitySink {
865 runtime: runtime.clone(),
866 live: sinks.turn_events(),
867 };
868 let turn = Box::pin(writer.stream_turn_with_agent_frames(
869 input,
870 turn_options(
871 sinks.events(),
872 &observation_sink,
873 scoped_effect_controller,
874 cancel,
875 ),
876 ))
877 .await?;
878 runtime.publish_from(&writer);
879 Ok(turn)
880}
881
882#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
883pub struct TurnResult {
884 pub state: SessionSnapshot,
885 pub outcome: TurnOutcome,
886 pub assistant_output: AssistantOutput,
887 pub usage: TokenUsage,
891 #[serde(default)]
895 pub children_usage: Vec<TokenLedgerEntry>,
896 #[serde(default)]
902 pub llm_calls: Vec<LlmCallRecord>,
903 pub tool_calls: Vec<ToolCallRecord>,
904 pub execution: ExecutionSummary,
905 pub errors: Vec<TurnIssue>,
906}
907
908impl TurnResult {
909 fn from_assembled(turn: lash_core::AssembledTurn) -> Self {
910 Self {
911 state: turn.state,
912 outcome: turn.outcome,
913 assistant_output: turn.assistant_output,
914 usage: turn.token_usage,
915 children_usage: turn.children_usage,
916 llm_calls: turn.llm_calls,
917 tool_calls: turn.tool_calls,
918 execution: turn.execution,
919 errors: turn.errors,
920 }
921 }
922
923 pub fn total_usage(&self) -> TokenUsage {
926 let mut total = self.usage.clone();
927 for entry in &self.children_usage {
928 total.add(&entry.usage);
929 }
930 total
931 }
932
933 pub fn started_at(&self) -> std::time::SystemTime {
938 std::time::UNIX_EPOCH + std::time::Duration::from_millis(self.execution.started_at_ms)
939 }
940
941 pub fn duration(&self) -> std::time::Duration {
945 std::time::Duration::from_millis(self.execution.duration_ms)
946 }
947
948 pub fn assistant_message(&self) -> Option<&str> {
949 match &self.outcome {
950 TurnOutcome::Finished(lash_core::TurnFinish::AssistantMessage { text }) => Some(text),
951 _ => None,
952 }
953 }
954
955 pub fn final_value(&self) -> Option<&serde_json::Value> {
956 match &self.outcome {
957 TurnOutcome::Finished(lash_core::TurnFinish::FinalValue { value }) => Some(value),
958 _ => None,
959 }
960 }
961
962 pub fn tool_value(&self) -> Option<(&str, &serde_json::Value)> {
963 match &self.outcome {
964 TurnOutcome::Finished(lash_core::TurnFinish::ToolValue { tool_name, value }) => {
965 Some((tool_name.as_str(), value))
966 }
967 _ => None,
968 }
969 }
970
971 pub fn is_success(&self) -> bool {
972 matches!(
973 self.outcome,
974 TurnOutcome::Finished(_) | TurnOutcome::AgentFrameSwitch { .. }
975 )
976 }
977}
978
979#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
980pub struct TurnOutput {
981 pub result: TurnResult,
982 pub activities: Vec<TurnActivity>,
983}
984
985impl TurnOutput {
986 pub fn assistant_message(&self) -> Option<&str> {
987 self.result.assistant_message()
988 }
989
990 pub fn final_value(&self) -> Option<&serde_json::Value> {
991 self.result.final_value()
992 }
993
994 pub fn tool_value(&self) -> Option<(&str, &serde_json::Value)> {
995 self.result.tool_value()
996 }
997
998 pub fn is_success(&self) -> bool {
999 self.result.is_success()
1000 }
1001}
1002
1003struct BorrowedTurnActivityFanout<'a> {
1004 live: &'a dyn TurnActivitySink,
1005 collector: &'a RunActivityCollector,
1006}
1007
1008#[async_trait]
1009impl TurnActivitySink for BorrowedTurnActivityFanout<'_> {
1010 async fn emit(&self, activity: TurnActivity) {
1011 self.live.emit(activity.clone()).await;
1012 self.collector.emit(activity).await;
1013 }
1014}
1015
1016#[derive(Default)]
1017pub(crate) struct RunActivityCollector {
1018 activities: Arc<StdMutex<Vec<TurnActivity>>>,
1019}
1020
1021impl RunActivityCollector {
1022 fn into_activities(self) -> Vec<TurnActivity> {
1023 self.activities
1024 .lock()
1025 .expect("run activity collector lock")
1026 .clone()
1027 }
1028
1029 #[cfg(all(test, feature = "rlm"))]
1030 pub(crate) fn snapshot(&self) -> Vec<TurnActivity> {
1031 self.activities
1032 .lock()
1033 .expect("run activity collector lock")
1034 .clone()
1035 }
1036}
1037
1038#[async_trait]
1039impl TurnActivitySink for RunActivityCollector {
1040 async fn emit(&self, activity: TurnActivity) {
1041 self.activities
1042 .lock()
1043 .expect("run activity collector lock")
1044 .push(activity);
1045 }
1046}
1047
1048pub struct TurnActivityFanout {
1049 sinks: Vec<Arc<dyn TurnActivitySink>>,
1050}
1051
1052impl TurnActivityFanout {
1053 pub fn new(sinks: impl IntoIterator<Item = Arc<dyn TurnActivitySink>>) -> Self {
1054 Self {
1055 sinks: sinks.into_iter().collect(),
1056 }
1057 }
1058}
1059
1060#[async_trait]
1061impl TurnActivitySink for TurnActivityFanout {
1062 async fn emit(&self, activity: TurnActivity) {
1063 for sink in &self.sinks {
1064 sink.emit(activity.clone()).await;
1065 }
1066 }
1067}
1068
1069pub fn message_text(message: &Message) -> String {
1070 message
1071 .parts
1072 .iter()
1073 .map(|part| part.content.as_str())
1074 .collect::<Vec<_>>()
1075 .join("\n")
1076}
1077
1078pub fn message_role(message: &Message) -> &'static str {
1079 match message.role {
1080 MessageRole::User => "user",
1081 MessageRole::Assistant => "assistant",
1082 MessageRole::System => "system",
1083 MessageRole::Event => "event",
1084 }
1085}