1use std::collections::BTreeSet;
4use std::sync::Arc;
5
6use serde::Serialize;
7use serde::Serializer;
8use serde::ser::SerializeMap;
9use serde_json::Value;
10
11use crate::BoxFuture;
12use crate::Error;
13use crate::Result;
14use crate::backend::checkpoint::{ExecutionOutcome, QueuedMessage as DurableQueuedMessage};
15use crate::backend::sandbox::Sandbox;
16use crate::protocol::EventMsg;
17use crate::protocol::FrontendActionListItem;
18use crate::protocol::FrontendBlock;
19use crate::protocol::FrontendBlockRole;
20use crate::protocol::FrontendContribution;
21use crate::protocol::FrontendEvent;
22use crate::protocol::FrontendSlot;
23use crate::protocol::FrontendTone;
24use crate::protocol::FrontendWidgetContent;
25use crate::protocol::RenderedBlock;
26use crate::protocol::ToolCallBeginEvent;
27use crate::protocol::ToolCallEndEvent;
28use crate::serialized_len;
29
30pub mod artifacts;
31pub mod attachments;
32pub mod compaction;
33pub mod computer_control;
34mod context;
35pub(crate) mod delivery_once;
36pub mod extensions;
37pub mod image_generation;
38pub mod instructions;
39pub mod manifest;
40pub mod messages;
41pub mod questions;
42pub mod scratchpad;
43pub mod sessions;
44pub mod subagents;
45pub mod tasks;
46pub mod tools;
47pub mod voice;
48
49pub use context::{
50 ActiveCommandContext, CompactContext, FrontendEventSink, MessageQueue, MessageRouteContext,
51 MessageSubmitContext, MiddlewareCommandContext, ModelContext, ModelRequestContext,
52 PermissionRequestContext, PostToolUseContext, PreToolUseContext, QueuedMessageSnapshot,
53 QueuedMessageView, RuntimeContext, SessionStartContext, SessionStartSource, StopContext,
54 SubmissionResult, ToolExposureContext, TurnEndContext, TurnIdentity,
55};
56pub(crate) use context::{MessageSubmitResult, PreparationNotices, PreparedMessage, StagedInput};
57use tools::Catalog;
58
59#[derive(Debug, Clone, Copy, PartialEq)]
62pub struct TokenEstimate {
63 bytes_per_token: f64,
64}
65
66impl Default for TokenEstimate {
67 fn default() -> Self {
68 #[derive(serde::Deserialize)]
69 struct Defaults {
70 bytes_per_token: f64,
71 }
72 static DEFAULTS: std::sync::LazyLock<Defaults> = std::sync::LazyLock::new(|| {
73 crate::config::embedded(include_str!("token_estimation.toml"))
74 });
75 Self::new(DEFAULTS.bytes_per_token)
76 .expect("bundled token estimate must be positive and finite")
77 }
78}
79
80impl TokenEstimate {
81 pub fn new(bytes_per_token: f64) -> Result<Self> {
85 if !bytes_per_token.is_finite() || bytes_per_token <= 0.0 {
86 return Err(Error::Config(
87 "bytes per token must be positive and finite".into(),
88 ));
89 }
90 Ok(Self { bytes_per_token })
91 }
92
93 #[must_use]
95 pub const fn bytes_per_token(self) -> f64 {
96 self.bytes_per_token
97 }
98
99 #[must_use]
101 pub fn tokens(self, bytes: usize) -> usize {
102 let tokens = (bytes as f64 / self.bytes_per_token).ceil();
103 if tokens >= usize::MAX as f64 {
104 return usize::MAX;
105 }
106 tokens as usize
108 }
109
110 #[must_use]
112 pub fn item_tokens(self, item: &Value) -> usize {
113 approximate_item_tokens(item, self)
114 }
115}
116
117pub struct MiddlewareCommandOutput {
119 pub events: Vec<FrontendEvent>,
121}
122
123#[derive(Clone)]
125pub struct FrontendExtensions {
126 stack: MiddlewareStack,
127 session_id: Arc<str>,
128}
129
130impl FrontendExtensions {
131 pub(crate) fn new(stack: MiddlewareStack, session_id: impl Into<Arc<str>>) -> Result<Self> {
132 let session_id = session_id.into();
133 stack.frontend(&session_id)?;
134 Ok(Self { stack, session_id })
135 }
136
137 pub fn contributions(&self) -> Result<Vec<FrontendContribution>> {
142 self.stack.declared_frontend(&self.session_id)
143 }
144
145 #[must_use]
147 pub fn context_limit(&self, context_window: i64) -> i64 {
148 self.stack
149 .entries
150 .iter()
151 .filter_map(|entry| entry.context_limit(context_window))
152 .min()
153 .unwrap_or(context_window)
154 }
155
156 #[must_use]
158 pub fn render(&self, event: &EventMsg) -> Vec<RenderedBlock> {
159 event
160 .presentation()
161 .into_iter()
162 .chain(self.stack.entries.iter().filter_map(|entry| {
163 entry
164 .render(event, &self.session_id)
165 .map(|block| RenderedBlock {
166 capability: entry.name().into(),
167 block,
168 })
169 }))
170 .collect()
171 }
172
173 pub async fn has_background_work(&self) -> Result<bool> {
178 for entry in &self.stack.entries {
179 if entry.has_background_work(&self.session_id).await? {
180 return Ok(true);
181 }
182 }
183 Ok(false)
184 }
185}
186
187impl MiddlewareCommandOutput {
188 #[must_use]
190 pub fn events(events: Vec<FrontendEvent>) -> Self {
191 Self { events }
192 }
193
194 #[must_use]
196 pub fn render(
197 capability: impl Into<String>,
198 text: impl Into<String>,
199 tone: FrontendTone,
200 ) -> Self {
201 let title = text.into();
202 Self::events(vec![FrontendEvent::Render {
203 capability: capability.into(),
204 block: FrontendBlock {
205 role: FrontendBlockRole::Notice,
206 title,
207 format: crate::protocol::FrontendBlockFormat::PlainText,
208 tone,
209 ..Default::default()
210 },
211 }])
212 }
213}
214
215pub trait Middleware: Send + Sync {
217 fn name(&self) -> &'static str;
219
220 fn incompatible_middleware(&self) -> &'static [&'static str] {
222 &[]
223 }
224
225 fn context_limit(&self, _context_window: i64) -> Option<i64> {
227 None
228 }
229
230 fn register(&self, _catalog: &mut Catalog, _runtime: &RuntimeContext) -> Result<()> {
235 Ok(())
236 }
237
238 fn prompt_section(&self, _runtime: &RuntimeContext) -> Result<Option<PromptSection>> {
243 Ok(None)
244 }
245
246 fn frontend(&self, _session_id: &str) -> FrontendContribution {
248 FrontendContribution::default()
249 }
250
251 fn render(&self, _event: &EventMsg, _session_id: &str) -> Option<FrontendBlock> {
255 None
256 }
257
258 fn command<'a>(
260 &'a self,
261 context: MiddlewareCommandContext<'a>,
262 ) -> BoxFuture<'a, Result<MiddlewareCommandOutput>> {
263 Box::pin(async move {
264 Err(Error::Unknown(format!(
265 "middleware command `{}/{}`",
266 self.name(),
267 context.command
268 )))
269 })
270 }
271
272 fn session_start<'a>(
274 &'a self,
275 _context: &'a mut SessionStartContext<'_>,
276 ) -> BoxFuture<'a, Result<()>> {
277 Box::pin(async { Ok(()) })
278 }
279
280 fn handles_messages(&self) -> bool {
282 false
283 }
284
285 fn route_message<'a>(
290 &'a self,
291 _context: &'a mut MessageRouteContext<'_>,
292 ) -> BoxFuture<'a, Result<SubmissionResult>> {
293 Box::pin(async move {
294 Err(Error::Config(format!(
295 "middleware `{}` claimed messages but did not prepare them",
296 self.name()
297 )))
298 })
299 }
300
301 fn message_boundary_events(&self, _submission_id: &str) -> Vec<EventMsg> {
303 Vec::new()
304 }
305
306 fn message_submit<'a>(
308 &'a self,
309 _context: &'a mut MessageSubmitContext<'_>,
310 ) -> BoxFuture<'a, Result<()>> {
311 Box::pin(async { Ok(()) })
312 }
313
314 fn active_command<'a>(
320 &'a self,
321 _context: &'a mut ActiveCommandContext<'_>,
322 ) -> BoxFuture<'a, Result<Option<SubmissionResult>>> {
323 Box::pin(async { Ok(None) })
324 }
325
326 fn pre_model<'a>(&'a self, _context: &'a mut ModelContext<'_>) -> BoxFuture<'a, Result<()>> {
328 Box::pin(async { Ok(()) })
329 }
330
331 fn model_request<'a>(
333 &'a self,
334 _context: &'a mut ModelRequestContext<'_>,
335 ) -> BoxFuture<'a, Result<()>> {
336 Box::pin(async { Ok(()) })
337 }
338
339 fn tool_exposure<'a>(
341 &'a self,
342 _context: &'a mut ToolExposureContext<'_>,
343 ) -> BoxFuture<'a, Result<()>> {
344 Box::pin(async { Ok(()) })
345 }
346
347 fn pre_tool_use<'a>(
349 &'a self,
350 _context: &'a mut PreToolUseContext<'_>,
351 ) -> BoxFuture<'a, Result<()>> {
352 Box::pin(async { Ok(()) })
353 }
354
355 fn permission_request<'a>(
357 &'a self,
358 _context: &'a mut PermissionRequestContext<'_>,
359 ) -> BoxFuture<'a, Result<()>> {
360 Box::pin(async { Ok(()) })
361 }
362
363 fn post_tool_use<'a>(
365 &'a self,
366 _context: &'a mut PostToolUseContext<'_>,
367 ) -> BoxFuture<'a, Result<()>> {
368 Box::pin(async { Ok(()) })
369 }
370
371 fn pre_compact<'a>(
373 &'a self,
374 _context: &'a mut CompactContext<'_>,
375 ) -> BoxFuture<'a, Result<()>> {
376 Box::pin(async { Ok(()) })
377 }
378
379 fn prepare_compacted_input(
382 &self,
383 _original: crate::backend::model::ModelInput<'_>,
384 _compacted: &mut Vec<Arc<Value>>,
385 ) {
386 }
387
388 fn post_compact<'a>(
390 &'a self,
391 _context: &'a mut CompactContext<'_>,
392 ) -> BoxFuture<'a, Result<()>> {
393 Box::pin(async { Ok(()) })
394 }
395
396 fn stop<'a>(&'a self, _context: &'a mut StopContext<'_>) -> BoxFuture<'a, Result<()>> {
398 Box::pin(async { Ok(()) })
399 }
400
401 fn turn_end<'a>(&'a self, _context: &'a mut TurnEndContext<'_>) -> BoxFuture<'a, Result<()>> {
403 Box::pin(async { Ok(()) })
404 }
405
406 fn session_end<'a>(&'a self, _runtime: &'a RuntimeContext) -> BoxFuture<'a, Result<()>> {
408 Box::pin(async { Ok(()) })
409 }
410
411 fn main_agent_only(&self) -> bool {
413 false
414 }
415
416 fn has_background_work<'a>(&'a self, _session_id: &'a str) -> BoxFuture<'a, Result<bool>> {
418 Box::pin(async { Ok(false) })
419 }
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
424pub struct PromptSection {
425 title: Option<&'static str>,
426 body: String,
427}
428
429impl PromptSection {
430 #[must_use]
432 pub fn new(body: impl Into<String>) -> Self {
433 Self {
434 title: None,
435 body: body.into(),
436 }
437 }
438
439 #[must_use]
441 pub fn titled(title: &'static str, body: impl Into<String>) -> Self {
442 Self {
443 title: Some(title),
444 body: body.into(),
445 }
446 }
447}
448
449#[derive(Clone)]
457pub struct MiddlewareStack {
458 entries: Vec<Arc<dyn Middleware>>,
459}
460
461#[derive(Debug)]
462pub(crate) struct SessionStartResult {
463 pub(crate) stop_reason: Option<String>,
464 pub(crate) input_changed: bool,
465}
466
467impl MiddlewareStack {
468 pub fn new(entries: Vec<Arc<dyn Middleware>>) -> Result<Self> {
473 let mut names = BTreeSet::new();
474 let mut message_handler = None;
475 for entry in &entries {
476 if !names.insert(entry.name()) {
477 return Err(Error::Duplicate(format!("middleware `{}`", entry.name())));
478 }
479 if entry.handles_messages()
480 && let Some(owner) = message_handler.replace(entry.name())
481 {
482 return Err(Error::Config(format!(
483 "conversation messages are owned by both `{owner}` and `{}`",
484 entry.name()
485 )));
486 }
487 }
488 for entry in &entries {
489 if let Some(other) = entry
490 .incompatible_middleware()
491 .iter()
492 .find(|name| names.contains(**name))
493 {
494 return Err(Error::Config(format!(
495 "middleware `{}` is incompatible with `{other}`",
496 entry.name()
497 )));
498 }
499 }
500 Ok(Self { entries })
501 }
502
503 pub(crate) fn for_child_agents(&self) -> Self {
504 Self {
506 entries: self
507 .entries
508 .iter()
509 .filter(|entry| !entry.main_agent_only())
510 .map(Arc::clone)
511 .collect(),
512 }
513 }
514
515 pub(crate) fn with_sandbox(mut self, sandbox: Arc<Sandbox>) -> Result<Self> {
516 self.entries.insert(0, sandbox);
517 Self::new(self.entries)
518 }
519
520 pub fn catalog(&self, runtime: &RuntimeContext) -> Result<Catalog> {
525 let mut catalog = Catalog::default();
526 for entry in &self.entries {
527 let previous = catalog.registered_definitions();
528 let registered = previous
529 .iter()
530 .map(|definition| definition.name.as_str())
531 .collect::<BTreeSet<_>>();
532 entry.register(&mut catalog, runtime)?;
533 for definition in catalog
534 .registered_definitions()
535 .iter()
536 .filter(|definition| !registered.contains(definition.name.as_str()))
537 {
538 validate_tool_rendering(entry.as_ref(), &definition.name, &runtime.session_id)?;
539 }
540 }
541 catalog.finalize()?;
542 Ok(catalog)
543 }
544
545 pub(crate) fn system_prompt(&self, base: &str, runtime: &RuntimeContext) -> Result<String> {
546 let mut prompt = format!("**instructions**\n\n{}", base.trim());
547 for entry in &self.entries {
548 let Some(section) = entry.prompt_section(runtime)? else {
549 continue;
550 };
551 let body = section.body.trim();
552 if body.is_empty() {
553 return Err(Error::Config(format!(
554 "middleware `{}` returned an empty prompt section",
555 entry.name()
556 )));
557 }
558 let title = section.title.unwrap_or_else(|| entry.name()).trim();
559 if title.is_empty() || title.lines().count() != 1 {
560 return Err(Error::Config(format!(
561 "middleware `{}` returned an invalid prompt section title",
562 entry.name()
563 )));
564 }
565 prompt.push_str("\n\n**");
566 prompt.push_str(title);
567 prompt.push_str("**\n\n");
568 prompt.push_str(body);
569 }
570 Ok(prompt)
571 }
572
573 pub fn frontend(&self, session_id: &str) -> Result<Vec<FrontendContribution>> {
578 let contributions = self.declared_frontend(session_id)?;
579 validate_frontend(&contributions)?;
580 Ok(contributions)
581 }
582
583 fn declared_frontend(&self, session_id: &str) -> Result<Vec<FrontendContribution>> {
584 let mut contributions = Vec::new();
585 for entry in &self.entries {
586 let contribution = entry.frontend(session_id);
587 if contribution.capability.is_empty()
588 && contribution.commands.is_empty()
589 && contribution.widgets.is_empty()
590 && contribution.references.is_empty()
591 {
592 continue;
593 }
594 if contribution.capability != entry.name() {
595 return Err(Error::Config(format!(
596 "middleware `{}` exported frontend metadata for `{}`",
597 entry.name(),
598 contribution.capability
599 )));
600 }
601 contributions.push(contribution);
602 }
603 Ok(contributions)
604 }
605
606 fn message_handler(&self) -> Option<&Arc<dyn Middleware>> {
607 self.entries.iter().find(|entry| entry.handles_messages())
608 }
609
610 fn message_handler_required(&self) -> Result<&Arc<dyn Middleware>> {
611 self.message_handler()
612 .ok_or_else(|| Error::Config("conversation message middleware is not installed".into()))
613 }
614
615 pub(crate) fn require_message_handler(&self) -> Result<()> {
616 self.message_handler_required().map(|_| ())
617 }
618
619 pub(crate) fn validate_message_queue(&self, queued: &[DurableQueuedMessage]) -> Result<()> {
620 let owner = self.message_handler_required()?.name();
621 if queued.iter().all(|item| item.owner() == owner) {
622 Ok(())
623 } else {
624 Err(Error::Checkpoint(
625 "queued message is owned by unavailable middleware".into(),
626 ))
627 }
628 }
629
630 pub(crate) async fn route_message(
631 &self,
632 context: &mut MessageRouteContext<'_>,
633 ) -> Result<SubmissionResult> {
634 let entry = self.message_handler_required()?;
635 context.queued_messages.scope(entry.name());
636 entry.route_message(context).await
637 }
638
639 pub(crate) async fn active_command(
640 &self,
641 middleware: &str,
642 context: &mut ActiveCommandContext<'_>,
643 ) -> Result<Option<SubmissionResult>> {
644 let Some(entry) = self.entries.iter().find(|entry| entry.name() == middleware) else {
645 return Ok(None);
646 };
647 if entry.handles_messages() {
648 context.queued_messages.scope(entry.name());
649 }
650 entry.active_command(context).await
651 }
652
653 pub(crate) fn stage_model_messages(
654 &self,
655 queued_messages: &mut Vec<DurableQueuedMessage>,
656 turn_id: &str,
657 ) -> Result<Vec<PreparedMessage>> {
658 let entry = self.message_handler_required()?;
659 let mut queue = MessageQueue::new(queued_messages);
660 queue.scope(entry.name());
661 let mut messages = queue.stage_model_messages(turn_id)?;
662 for message in &mut messages {
663 message.boundary_events = entry.message_boundary_events(&message.submission_id);
664 }
665 Ok(messages)
666 }
667
668 pub(crate) fn messages_ready(
669 &self,
670 queued_messages: &[DurableQueuedMessage],
671 turn_id: &str,
672 ) -> Result<bool> {
673 let entry = self.message_handler_required()?;
674 Ok(queued_messages.iter().any(|item| {
675 item.owner() == entry.name()
676 && matches!(
677 item.boundary(),
678 crate::backend::checkpoint::QueuedMessageBoundary::Steer {
679 turn_id: target
680 } if target == turn_id
681 )
682 }))
683 }
684
685 pub(crate) fn next_turn(
686 &self,
687 queued_messages: &mut Vec<DurableQueuedMessage>,
688 ) -> Result<Option<PreparedMessage>> {
689 let entry = self.message_handler_required()?;
690 let mut queue = MessageQueue::new(queued_messages);
691 queue.scope(entry.name());
692 Ok(queue.next_turn()?.map(|mut message| {
693 message.boundary_events = entry.message_boundary_events(&message.submission_id);
694 message
695 }))
696 }
697
698 pub(crate) fn consume_next_turn(
699 &self,
700 queued_messages: &mut Vec<DurableQueuedMessage>,
701 submission_id: &str,
702 ) -> Result<(usize, DurableQueuedMessage)> {
703 let entry = self.message_handler_required()?;
704 let mut queue = MessageQueue::new(queued_messages);
705 queue.scope(entry.name());
706 queue.consume_next_turn(submission_id)
707 }
708
709 pub(crate) async fn message_submit(
710 &self,
711 turn: TurnIdentity<'_>,
712 message: &PreparedMessage,
713 events: &mut Vec<EventMsg>,
714 receipts: &delivery_once::Receipts,
715 ) -> Result<MessageSubmitResult> {
716 let event = message
717 .event
718 .message()
719 .ok_or_else(|| Error::Checkpoint("prepared event is not a message".into()))?;
720 let mut context = MessageSubmitContext {
721 turn,
722 author: &event.author,
723 message: &event.text,
724 attachments: &event.attachments,
725 events,
726 delivery_once: delivery_once::DeliveryOnce::new(receipts),
727 input: Vec::new(),
728 rejection: None,
729 };
730 for entry in &self.entries {
731 context.delivery_once.owner = entry.name();
732 entry.message_submit(&mut context).await?;
733 }
734 Ok(MessageSubmitResult {
735 input: context.input,
736 rejection: context.rejection,
737 })
738 }
739
740 pub(crate) fn finish_message_turn(
741 &self,
742 queued_messages: &mut Vec<DurableQueuedMessage>,
743 turn_id: &str,
744 outcome: ExecutionOutcome,
745 ) -> Result<()> {
746 let entry = self.message_handler_required()?;
747 if outcome == ExecutionOutcome::Completed {
748 return Ok(());
749 }
750 let mut queue = MessageQueue::new(queued_messages);
751 queue.scope(entry.name());
752 queue.promote_failed_turn(turn_id)
753 }
754
755 pub(crate) async fn session_start(
756 &self,
757 runtime: &RuntimeContext,
758 queued_messages: &[DurableQueuedMessage],
759 source: SessionStartSource,
760 input: &mut Vec<Arc<Value>>,
761 receipts: &mut delivery_once::Receipts,
762 ) -> Result<SessionStartResult> {
763 let compact_input = (source == SessionStartSource::Compact)
764 .then(|| input.iter().map(Arc::clone).collect::<Vec<_>>());
765 let mut context = SessionStartContext {
766 runtime,
767 delivery_once: delivery_once::DeliveryOnce::new(receipts),
768 source,
769 queued_messages: QueuedMessageSnapshot::default(),
770 input,
771 input_changed: false,
772 stop_reason: None,
773 };
774 for (index, entry) in self.entries.iter().enumerate() {
775 context.delivery_once.owner = entry.name();
776 context.queued_messages =
777 QueuedMessageSnapshot::for_owner(entry.name(), queued_messages);
778 if let Err(error) = entry.session_start(&mut context).await {
779 if let Some(input) = compact_input {
780 *context.input = input;
781 return Err(error);
782 }
783 let mut rollback_error = None;
784 for started in self.entries[..index].iter().rev() {
785 if let Err(error) = started.session_end(runtime).await
786 && rollback_error.is_none()
787 {
788 rollback_error = Some(error);
789 }
790 }
791 return Err(match rollback_error {
792 Some(rollback) => Error::Rollback {
793 primary: Box::new(error),
794 rollback: Box::new(rollback),
795 },
796 None => error,
797 });
798 }
799 }
800 let mut result = SessionStartResult {
801 stop_reason: context.stop_reason,
802 input_changed: context.input_changed,
803 };
804 result.input_changed |= delivery_once::record(receipts, input);
805 Ok(result)
806 }
807
808 pub(crate) async fn prepare_model(
809 &self,
810 mut context: ModelContext<'_>,
811 ) -> Result<Option<Vec<Value>>> {
812 let supports_tool_image_input =
813 context.model.supports_tool_image_input(context.provider)?;
814 self.resolve_tool_exposure(
815 context.session_id,
816 supports_tool_image_input,
817 context.durable_input.input(),
818 context.available_tools,
819 )
820 .await?;
821 for entry in &self.entries {
822 entry.pre_model(&mut context).await?;
823 }
824 if context.turn_stopped() {
825 return Ok(None);
826 }
827 let mut request_context = ModelRequestContext {
828 author: context.author,
829 role: &context.runtime.role,
830 model: context.model,
831 provider: context.provider,
832 session_id: context.session_id,
833 turn_id: context.turn_id,
834 model_step: context.model_step,
835 input: context.input(),
836 replacement: None,
837 };
838 for entry in &self.entries {
839 entry.model_request(&mut request_context).await?;
840 }
841 Ok(request_context.replacement)
842 }
843
844 pub(crate) async fn resolve_tool_exposure(
845 &self,
846 session_id: &str,
847 supports_tool_image_input: bool,
848 input: crate::backend::model::ModelInput<'_>,
849 available: &mut BTreeSet<String>,
850 ) -> Result<()> {
851 for entry in &self.entries {
852 entry
853 .tool_exposure(&mut ToolExposureContext {
854 session_id,
855 supports_tool_image_input,
856 input,
857 available,
858 })
859 .await?;
860 }
861 Ok(())
862 }
863
864 pub(crate) async fn pre_tool_use(&self, context: &mut PreToolUseContext<'_>) -> Result<()> {
865 for entry in &self.entries {
866 context.delivery_once.owner = entry.name();
867 entry.pre_tool_use(context).await?;
868 }
869 Ok(())
870 }
871
872 pub(crate) async fn permission_request(
873 &self,
874 context: &mut PermissionRequestContext<'_>,
875 ) -> Result<()> {
876 for entry in &self.entries {
877 entry.permission_request(context).await?;
878 }
879 Ok(())
880 }
881
882 pub(crate) async fn post_tool_use(&self, context: &mut PostToolUseContext<'_>) -> Result<()> {
883 for entry in &self.entries {
884 context.delivery_once.owner = entry.name();
885 entry.post_tool_use(context).await?;
886 }
887 Ok(())
888 }
889
890 pub(crate) async fn pre_compact(
891 &self,
892 mut context: CompactContext<'_>,
893 ) -> Result<Option<String>> {
894 for entry in &self.entries {
895 entry.pre_compact(&mut context).await?;
896 }
897 Ok(context.stop_reason)
898 }
899
900 pub(crate) fn prepare_compacted_input(
901 &self,
902 original: crate::backend::model::ModelInput<'_>,
903 compacted: &mut Vec<Arc<Value>>,
904 ) {
905 for entry in &self.entries {
906 entry.prepare_compacted_input(original, compacted);
907 }
908 }
909
910 pub(crate) async fn post_compact(
911 &self,
912 mut context: CompactContext<'_>,
913 ) -> Result<Option<String>> {
914 for entry in &self.entries {
915 entry.post_compact(&mut context).await?;
916 }
917 Ok(context.stop_reason)
918 }
919
920 pub(crate) async fn stop(&self, context: &mut StopContext<'_>) -> Result<()> {
921 for entry in &self.entries {
922 entry.stop(context).await?;
923 }
924 Ok(())
925 }
926
927 pub(crate) async fn turn_end(&self, mut context: TurnEndContext<'_>) -> Result<()> {
928 for entry in &self.entries {
929 context.owner = Some(entry.name());
930 entry.turn_end(&mut context).await?;
931 }
932 Ok(())
933 }
934
935 pub(crate) async fn session_end(&self, runtime: &RuntimeContext) -> Result<()> {
936 let mut first_error = None;
937 for entry in self.entries.iter().rev() {
938 if let Err(error) = entry.session_end(runtime).await
939 && first_error.is_none()
940 {
941 first_error = Some(error);
942 }
943 }
944 first_error.map_or(Ok(()), Err)
945 }
946
947 pub(crate) async fn command(
948 &self,
949 middleware: &str,
950 context: MiddlewareCommandContext<'_>,
951 ) -> Result<MiddlewareCommandOutput> {
952 let entry = self
953 .entries
954 .iter()
955 .find(|entry| entry.name() == middleware)
956 .ok_or_else(|| Error::Unknown(format!("middleware `{middleware}`")))?;
957 let declared = entry
958 .frontend(context.session_id)
959 .commands
960 .into_iter()
961 .any(|command| command.name == context.command);
962 if !declared {
963 return Err(Error::Unknown(format!(
964 "middleware command `{middleware}/{}`",
965 context.command
966 )));
967 }
968 entry.command(context).await
969 }
970}
971
972fn validate_tool_rendering(
973 middleware: &dyn Middleware,
974 tool_name: &str,
975 session_id: &str,
976) -> Result<()> {
977 let events = [
978 (
979 "ToolCallBegin",
980 EventMsg::ToolCallBegin(ToolCallBeginEvent {
981 turn_id: "validation".into(),
982 call_id: "validation".into(),
983 name: tool_name.into(),
984 arguments: serde_json::json!({}),
985 }),
986 ),
987 (
988 "successful ToolCallEnd",
989 EventMsg::ToolCallEnd(ToolCallEndEvent {
990 turn_id: "validation".into(),
991 call_id: "validation".into(),
992 name: tool_name.into(),
993 output: String::new().into(),
994 is_error: false,
995 }),
996 ),
997 (
998 "error ToolCallEnd",
999 EventMsg::ToolCallEnd(ToolCallEndEvent {
1000 turn_id: "validation".into(),
1001 call_id: "validation".into(),
1002 name: tool_name.into(),
1003 output: "validation error".into(),
1004 is_error: true,
1005 }),
1006 ),
1007 ];
1008 for (event_name, event) in events {
1009 if middleware.render(&event, session_id).is_none() {
1010 return Err(Error::Config(format!(
1011 "middleware `{}` registered tool `{tool_name}` but does not render `{event_name}`",
1012 middleware.name()
1013 )));
1014 }
1015 }
1016 Ok(())
1017}
1018
1019fn validate_frontend(contributions: &[FrontendContribution]) -> Result<()> {
1020 let mut commands = BTreeSet::new();
1021 let mut widgets = BTreeSet::new();
1022 let mut references = BTreeSet::new();
1023 for contribution in contributions {
1024 for command in &contribution.commands {
1025 if command.name.is_empty() || command.name.chars().any(char::is_whitespace) {
1026 return Err(Error::Config(format!(
1027 "invalid frontend command `{}`",
1028 command.name
1029 )));
1030 }
1031 if !commands.insert(command.name.as_str()) {
1032 return Err(Error::Duplicate(format!(
1033 "frontend command `{}`",
1034 command.name
1035 )));
1036 }
1037 }
1038 for item in &contribution.widgets {
1039 if item.id.is_empty()
1040 || !widgets.insert((contribution.capability.as_str(), item.id.as_str()))
1041 {
1042 return Err(Error::Duplicate(format!(
1043 "frontend status `{}/{}`",
1044 contribution.capability, item.id
1045 )));
1046 }
1047 if matches!(item.slot, FrontendSlot::Navigation | FrontendSlot::ChatMenu)
1048 && (item.text.trim().is_empty()
1049 || (item.content.is_none() && item.action.is_none()))
1050 {
1051 return Err(Error::Config(format!(
1052 "frontend surface `{}/{}` requires a label and content or action",
1053 contribution.capability, item.id
1054 )));
1055 }
1056 if let Some(FrontendWidgetContent::ActionList {
1057 title,
1058 items,
1059 actions,
1060 }) = &item.content
1061 {
1062 validate_action_list(title, items)?;
1063 validate_actions(actions)?;
1064 }
1065 }
1066 for reference in &contribution.references {
1067 if reference.trigger.is_control()
1068 || reference.trigger.is_whitespace()
1069 || reference.value.is_empty()
1070 || reference.value.chars().any(char::is_whitespace)
1071 {
1072 return Err(Error::Config(format!(
1073 "invalid frontend reference `{}{}`",
1074 reference.trigger, reference.value
1075 )));
1076 }
1077 if !references.insert((reference.trigger, reference.value.as_str())) {
1078 return Err(Error::Duplicate(format!(
1079 "frontend reference `{}{}`",
1080 reference.trigger, reference.value
1081 )));
1082 }
1083 }
1084 }
1085 Ok(())
1086}
1087
1088fn validate_action_list(title: &str, items: &[FrontendActionListItem]) -> Result<()> {
1089 if title.trim().is_empty() {
1090 return Err(Error::Config("frontend action list title is empty".into()));
1091 }
1092 let mut item_ids = BTreeSet::new();
1093 for item in items {
1094 if item.id.trim().is_empty() || item.text.trim().is_empty() {
1095 return Err(Error::Config(
1096 "frontend action list item requires an ID and text".into(),
1097 ));
1098 }
1099 if !item_ids.insert(&item.id) {
1100 return Err(Error::Duplicate(format!(
1101 "frontend action list item `{}`",
1102 item.id
1103 )));
1104 }
1105 validate_actions(&item.actions)?;
1106 }
1107 Ok(())
1108}
1109
1110fn validate_actions(actions: &[crate::protocol::FrontendAction]) -> Result<()> {
1111 let mut action_ids = BTreeSet::new();
1112 for action in actions {
1113 if action.id.trim().is_empty()
1114 || action.label.trim().is_empty()
1115 || action.symbol.as_str().trim().is_empty()
1116 || (action.input_from_label
1117 && (action.editor.is_some()
1118 || !matches!(
1119 action.op,
1120 crate::protocol::Op::CapabilityCommand { input: None, .. }
1121 )))
1122 || action.editor.as_ref().is_some_and(|editor| {
1123 editor.title.trim().is_empty()
1124 || editor.label.trim().is_empty()
1125 || editor.submit_label.trim().is_empty()
1126 })
1127 {
1128 return Err(Error::Config(
1129 "frontend list action requires an ID, label, and symbol".into(),
1130 ));
1131 }
1132 if !action_ids.insert(&action.id) {
1133 return Err(Error::Duplicate(format!(
1134 "frontend list action `{}`",
1135 action.id
1136 )));
1137 }
1138 }
1139 Ok(())
1140}
1141
1142fn approximate_item_tokens(item: &Value, estimate: TokenEstimate) -> usize {
1143 let image_tokens = crate::protocol::content_parts(item)
1144 .into_iter()
1145 .flatten()
1146 .filter(|part| part.get("type").and_then(Value::as_str) == Some("input_image"))
1147 .fold(0usize, |total, part| {
1148 let width = part["image"]["width"].as_u64().unwrap_or(2048);
1149 let height = part["image"]["height"].as_u64().unwrap_or(2048);
1150 let tokens = width
1152 .div_ceil(512)
1153 .saturating_mul(height.div_ceil(512))
1154 .saturating_mul(256)
1155 .saturating_add(256);
1156 total.saturating_add(usize::try_from(tokens).unwrap_or(usize::MAX))
1157 });
1158 serialized_len(&PublicItem(item))
1159 .map_or(0, |bytes| estimate.tokens(bytes))
1160 .saturating_add(image_tokens)
1161 .max(1)
1162}
1163
1164struct PublicItem<'a>(&'a Value);
1165
1166impl Serialize for PublicItem<'_> {
1167 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1168 where
1169 S: Serializer,
1170 {
1171 let Some(fields) = self.0.as_object() else {
1172 return self.0.serialize(serializer);
1173 };
1174 let mut map = serializer.serialize_map(None)?;
1175 for (name, value) in fields.iter().filter(|(name, _)| !name.starts_with('_')) {
1176 if let Some(parts) = value.as_array()
1177 && crate::protocol::content_parts(self.0)
1178 .is_some_and(|content| std::ptr::eq(parts, content))
1179 {
1180 map.serialize_entry(name, &ImageTokenContent(parts))?;
1181 } else {
1182 map.serialize_entry(name, value)?;
1183 }
1184 }
1185 map.end()
1186 }
1187}
1188
1189struct ImageTokenContent<'a>(&'a [Value]);
1190
1191impl Serialize for ImageTokenContent<'_> {
1192 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1193 where
1194 S: Serializer,
1195 {
1196 serializer.collect_seq(self.0.iter().map(|part| {
1197 (part.get("type").and_then(Value::as_str) != Some("input_image")).then_some(part)
1198 }))
1199 }
1200}
1201
1202#[cfg(test)]
1203mod tests;