winx_code_agent/state/execution_history/
observation.rs1use schemars::JsonSchema;
4use serde::Deserialize;
5
6use super::{
7 paging, storage, validate_id, ExecutionCatalog, ExecutionRecord, ExecutionScope, HistoryError,
8 HistoryResult, OutputPage, OutputSource, MAX_PAGE_BYTES, MAX_WATCHED_EXECUTIONS,
9};
10
11pub const DEFAULT_OBSERVATION_PAGE_BYTES: usize = 2 * 1024;
12pub const MAX_OBSERVATION_ITEMS: usize = MAX_WATCHED_EXECUTIONS;
13pub const MAX_OBSERVATION_OUTPUT_BYTES: usize = MAX_PAGE_BYTES;
14
15#[derive(Clone, Debug, Deserialize, JsonSchema)]
16pub struct ObservationRequest {
17 #[schemars(length(min = 1, max = 512))]
19 pub execution_id: String,
20 #[serde(default)]
21 pub source: OutputSource,
22 #[serde(default)]
24 pub cursor: Option<String>,
25 #[serde(default = "default_page_bytes")]
27 #[schemars(range(min = 4, max = 65536))]
28 pub page_bytes: usize,
29}
30
31const fn default_page_bytes() -> usize {
32 DEFAULT_OBSERVATION_PAGE_BYTES
33}
34
35#[derive(Debug)]
37pub struct ObservationItem {
38 pub execution_id: String,
39 pub source: OutputSource,
40 pub record: Option<ExecutionRecord>,
41 pub page: HistoryResult<OutputPage>,
42}
43
44#[derive(Debug)]
46pub struct CatalogObservation {
47 pub sequence: u64,
48 pub cursor: String,
49 pub items: Vec<ObservationItem>,
50}
51
52pub fn validate_observation_requests(items: &[ObservationRequest]) -> HistoryResult<()> {
57 if items.is_empty() || items.len() > MAX_OBSERVATION_ITEMS {
58 return Err(HistoryError::InvalidInput(format!(
59 "observe requires between 1 and {MAX_OBSERVATION_ITEMS} items"
60 )));
61 }
62 let mut total_page_bytes = 0;
63 for (index, item) in items.iter().enumerate() {
64 validate_id(&item.execution_id)?;
65 if !(4..=MAX_PAGE_BYTES).contains(&item.page_bytes) {
66 return Err(HistoryError::InvalidInput(format!(
67 "observe page_bytes must be between 4 and {MAX_PAGE_BYTES}"
68 )));
69 }
70 total_page_bytes += item.page_bytes;
72 if total_page_bytes > MAX_OBSERVATION_OUTPUT_BYTES {
73 return Err(HistoryError::InvalidInput(format!(
74 "observe total page_bytes must not exceed {MAX_OBSERVATION_OUTPUT_BYTES}"
75 )));
76 }
77 if items[..index].iter().any(|previous| {
78 previous.execution_id == item.execution_id && previous.source == item.source
79 }) {
80 return Err(HistoryError::InvalidInput(
81 "observe requires unique execution_id and source pairs".into(),
82 ));
83 }
84 }
85 Ok(())
86}
87
88impl ExecutionCatalog {
89 pub fn observe(
95 &self,
96 scope: &ExecutionScope,
97 items: &[ObservationRequest],
98 now: u64,
99 ) -> HistoryResult<CatalogObservation> {
100 validate_observation_requests(items)?;
101 storage::transaction(self, scope, now, |transaction| {
102 let items = items
103 .iter()
104 .map(|item| ObservationItem {
105 execution_id: item.execution_id.clone(),
106 source: item.source,
107 record: transaction.find(&item.execution_id).cloned(),
108 page: paging::read(
109 transaction,
110 &item.execution_id,
111 item.source,
112 item.cursor.as_deref(),
113 item.page_bytes,
114 ),
115 })
116 .collect();
117 Ok(CatalogObservation {
118 sequence: transaction.index.sequence,
119 cursor: paging::event_cursor(transaction)?,
120 items,
121 })
122 })
123 }
124}