1use crate::checkpoint_data::CheckpointData;
7use crate::checkpointer::Checkpointer;
8use crate::command::Command;
9use crate::config::GraphConfig;
10use crate::graph::StateGraph;
11use crate::pregel::PregelEngine;
12use crate::snapshot::StateSnapshot;
13use pe_core::error::PeError;
14use pe_core::lobe::LobeRuntimeServiceFactory;
15use pe_core::node::InterruptRequest;
16use pe_core::state::State;
17use std::any::Any;
18use std::sync::Arc;
19
20#[derive(Debug, Clone)]
22#[non_exhaustive]
23pub enum ExecutionOutcome<S: State> {
24 Completed(S),
26
27 Interrupted {
29 state: S,
31 request: InterruptRequest<S::Update>,
33 },
34}
35
36pub struct CompiledGraph<S: State> {
54 pub(crate) graph: Arc<StateGraph<S>>,
55 checkpointer: Option<Arc<dyn Checkpointer>>,
56 matrix_hook: Option<crate::matrix_hook::MatrixHookHandle>,
58 agent: Option<pe_core::agent::Agent>,
60}
61
62impl<S: State> std::fmt::Debug for CompiledGraph<S> {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("CompiledGraph")
65 .field("nodes", &self.graph.nodes.keys().collect::<Vec<_>>())
66 .field("has_checkpointer", &self.checkpointer.is_some())
67 .field("has_matrix_hook", &self.matrix_hook.is_some())
68 .field("has_agent", &self.agent.is_some())
69 .finish()
70 }
71}
72
73impl<S: State> CompiledGraph<S> {
74 pub(crate) fn new(graph: Arc<StateGraph<S>>) -> Self {
76 Self {
77 graph,
78 checkpointer: None,
79 agent: None,
80 matrix_hook: None,
81 }
82 }
83
84 pub fn with_checkpointer(mut self, cp: impl Checkpointer + 'static) -> Self {
88 self.checkpointer = Some(Arc::new(cp));
89 self
90 }
91
92 pub fn with_agent(mut self, agent: pe_core::agent::Agent) -> Self {
97 self.agent = Some(agent);
98 self
99 }
100
101 pub fn agent(&self) -> Option<&pe_core::agent::Agent> {
103 self.agent.as_ref()
104 }
105
106 pub fn with_checkpointer_arc(mut self, cp: Arc<dyn Checkpointer>) -> Self {
108 self.checkpointer = Some(cp);
109 self
110 }
111
112 pub fn with_matrix_hook(mut self, hook: crate::matrix_hook::MatrixHookHandle) -> Self {
122 self.matrix_hook = Some(hook);
123 self
124 }
125
126 pub fn matrix_hook(&self) -> Option<&crate::matrix_hook::MatrixHookHandle> {
128 self.matrix_hook.as_ref()
129 }
130
131 #[must_use = "the execution outcome contains the final state"]
139 pub async fn invoke(
140 &self,
141 state: S,
142 config: GraphConfig,
143 ) -> Result<ExecutionOutcome<S>, PeError> {
144 self.invoke_with_lobe_runtime_services(state, config, None)
145 .await
146 }
147
148 #[must_use = "the execution outcome contains the final state"]
150 pub async fn invoke_with_lobe_runtime_services(
151 &self,
152 state: S,
153 config: GraphConfig,
154 lobe_runtime_service_factory: Option<Arc<dyn LobeRuntimeServiceFactory>>,
155 ) -> Result<ExecutionOutcome<S>, PeError> {
156 self.invoke_with_observer_and_lobe_runtime_services(
157 state,
158 config,
159 None,
160 None,
161 lobe_runtime_service_factory,
162 )
163 .await
164 }
165
166 #[must_use = "the execution outcome contains the final state"]
168 pub async fn invoke_with_observer_and_lobe_runtime_services(
169 &self,
170 state: S,
171 config: GraphConfig,
172 observer: Option<Arc<dyn pe_core::node::NodeObserver>>,
173 tool_observer: Option<Arc<dyn pe_core::node::ToolObserver>>,
174 lobe_runtime_service_factory: Option<Arc<dyn LobeRuntimeServiceFactory>>,
175 ) -> Result<ExecutionOutcome<S>, PeError> {
176 if let Some(ref cp_id) = config.checkpoint_id {
178 let data = self.load_checkpoint_data(&config.thread_id, cp_id).await?;
179 let mut engine = self.make_engine(config);
180 if let Some(obs) = observer.clone() {
181 engine = engine.with_observer(obs);
182 }
183 if let Some(tobs) = tool_observer.clone() {
184 engine = engine.with_tool_observer(tobs);
185 }
186 if let Some(factory) = lobe_runtime_service_factory {
187 engine = engine.with_lobe_runtime_service_factory(factory);
188 }
189 return engine.run_from_checkpoint(data).await;
190 }
191
192 let mut engine = self.make_engine(config);
193 if let Some(obs) = observer {
194 engine = engine.with_observer(obs);
195 }
196 if let Some(tobs) = tool_observer {
197 engine = engine.with_tool_observer(tobs);
198 }
199 if let Some(factory) = lobe_runtime_service_factory {
200 engine = engine.with_lobe_runtime_service_factory(factory);
201 }
202 engine.run(state).await
203 }
204
205 #[must_use = "the execution outcome contains the final state"]
216 pub async fn invoke_with_stream(
217 &self,
218 state: S,
219 config: GraphConfig,
220 stream_sender: Arc<dyn Any + Send + Sync>,
221 ) -> Result<ExecutionOutcome<S>, PeError> {
222 self.invoke_with_stream_and_observer(state, config, stream_sender, None, None)
223 .await
224 }
225
226 #[must_use = "the execution outcome contains the final state"]
232 pub async fn invoke_with_stream_and_observer(
233 &self,
234 state: S,
235 config: GraphConfig,
236 stream_sender: Arc<dyn Any + Send + Sync>,
237 observer: Option<Arc<dyn pe_core::node::NodeObserver>>,
238 tool_observer: Option<Arc<dyn pe_core::node::ToolObserver>>,
239 ) -> Result<ExecutionOutcome<S>, PeError> {
240 self.invoke_with_stream_observer_and_lobe_runtime_services(
241 state,
242 config,
243 stream_sender,
244 observer,
245 tool_observer,
246 None,
247 )
248 .await
249 }
250
251 #[must_use = "the execution outcome contains the final state"]
253 pub async fn invoke_with_stream_observer_and_lobe_runtime_services(
254 &self,
255 state: S,
256 config: GraphConfig,
257 stream_sender: Arc<dyn Any + Send + Sync>,
258 observer: Option<Arc<dyn pe_core::node::NodeObserver>>,
259 tool_observer: Option<Arc<dyn pe_core::node::ToolObserver>>,
260 lobe_runtime_service_factory: Option<Arc<dyn LobeRuntimeServiceFactory>>,
261 ) -> Result<ExecutionOutcome<S>, PeError> {
262 if let Some(ref cp_id) = config.checkpoint_id {
264 let data = self.load_checkpoint_data(&config.thread_id, cp_id).await?;
265 let mut engine = self.make_engine(config).with_stream_sender(stream_sender);
266 if let Some(obs) = observer {
267 engine = engine.with_observer(obs);
268 }
269 if let Some(tobs) = tool_observer {
270 engine = engine.with_tool_observer(tobs);
271 }
272 if let Some(factory) = lobe_runtime_service_factory {
273 engine = engine.with_lobe_runtime_service_factory(factory);
274 }
275 return engine.run_from_checkpoint(data).await;
276 }
277
278 let mut engine = self.make_engine(config).with_stream_sender(stream_sender);
279 if let Some(obs) = observer {
280 engine = engine.with_observer(obs);
281 }
282 if let Some(tobs) = tool_observer {
283 engine = engine.with_tool_observer(tobs);
284 }
285 if let Some(factory) = lobe_runtime_service_factory {
286 engine = engine.with_lobe_runtime_service_factory(factory);
287 }
288 engine.run(state).await
289 }
290
291 #[must_use = "the execution outcome contains the final state"]
297 pub async fn resume(
298 &self,
299 thread_id: &str,
300 input: S::Update,
301 config: GraphConfig,
302 ) -> Result<ExecutionOutcome<S>, PeError> {
303 if thread_id != config.thread_id {
304 return Err(PeError::GraphValue {
305 details: format!(
306 "resume() thread_id '{}' does not match config.thread_id '{}'",
307 thread_id, config.thread_id
308 ),
309 });
310 }
311
312 let cp = self.checkpointer.as_ref().ok_or(PeError::Storage {
313 details: "Cannot resume without a checkpointer".into(),
314 })?;
315
316 let (bytes, meta) =
317 cp.load_latest(thread_id)
318 .await?
319 .ok_or(PeError::CheckpointNotFound {
320 thread_id: thread_id.to_string(),
321 })?;
322
323 let mut data: CheckpointData<S> =
324 serde_json::from_slice(&bytes).map_err(|e| PeError::Storage {
325 details: format!("Checkpoint deserialization failed: {e}"),
326 })?;
327 data.checkpoint_id = Some(meta.id.clone());
329
330 data.state.apply(input);
332
333 if let Some(ref interrupted) = data.interrupted_node {
336 let successors = self.graph.fixed_successors(interrupted);
337 if !successors.is_empty() {
338 data.next_nodes = successors;
339 }
340 }
341
342 self.make_engine(config).run_from_checkpoint(data).await
343 }
344
345 #[must_use = "the execution outcome contains the final state"]
361 pub async fn resume_with(
362 &self,
363 thread_id: &str,
364 command: Command,
365 config: GraphConfig,
366 ) -> Result<ExecutionOutcome<S>, PeError> {
367 if thread_id != config.thread_id {
368 return Err(PeError::GraphValue {
369 details: format!(
370 "resume() thread_id '{}' does not match config.thread_id '{}'",
371 thread_id, config.thread_id
372 ),
373 });
374 }
375
376 let data = self.load_and_apply_command(thread_id, command).await?;
377 self.make_engine(config).run_from_checkpoint(data).await
378 }
379
380 #[must_use = "the execution outcome contains the final state"]
385 pub async fn resume_with_stream(
386 &self,
387 thread_id: &str,
388 command: Command,
389 config: GraphConfig,
390 stream_sender: Arc<dyn Any + Send + Sync>,
391 observer: Option<Arc<dyn pe_core::node::NodeObserver>>,
392 tool_observer: Option<Arc<dyn pe_core::node::ToolObserver>>,
393 ) -> Result<ExecutionOutcome<S>, PeError> {
394 if thread_id != config.thread_id {
395 return Err(PeError::GraphValue {
396 details: format!(
397 "resume() thread_id '{}' does not match config.thread_id '{}'",
398 thread_id, config.thread_id
399 ),
400 });
401 }
402
403 let data = self.load_and_apply_command(thread_id, command).await?;
404 let mut engine = self.make_engine(config).with_stream_sender(stream_sender);
405 if let Some(obs) = observer {
406 engine = engine.with_observer(obs);
407 }
408 if let Some(tobs) = tool_observer {
409 engine = engine.with_tool_observer(tobs);
410 }
411 engine.run_from_checkpoint(data).await
412 }
413
414 fn make_engine(&self, config: GraphConfig) -> PregelEngine<S> {
416 let mut engine =
417 PregelEngine::new(Arc::clone(&self.graph), config, self.checkpointer.clone());
418 if let Some(ref hook) = self.matrix_hook {
419 engine = engine.with_matrix_hook(hook.clone());
420 }
421 engine
422 }
423
424 async fn load_checkpoint_data(
429 &self,
430 thread_id: &str,
431 checkpoint_id: &str,
432 ) -> Result<CheckpointData<S>, PeError> {
433 let cp = self.checkpointer.as_ref().ok_or(PeError::Storage {
434 details: "Cannot time-travel without a checkpointer".into(),
435 })?;
436 let bytes =
437 cp.load_by_id(thread_id, checkpoint_id)
438 .await?
439 .ok_or(PeError::CheckpointNotFound {
440 thread_id: format!("{}@{}", thread_id, checkpoint_id),
441 })?;
442 serde_json::from_slice(&bytes).map_err(|e| PeError::Storage {
443 details: format!("Checkpoint deserialization failed: {e}"),
444 })
445 }
446
447 async fn load_and_apply_command(
454 &self,
455 thread_id: &str,
456 command: Command,
457 ) -> Result<CheckpointData<S>, PeError> {
458 let cp = self.checkpointer.as_ref().ok_or(PeError::Storage {
459 details: "Cannot resume without a checkpointer".into(),
460 })?;
461
462 let (bytes, meta) =
463 cp.load_latest(thread_id)
464 .await?
465 .ok_or(PeError::CheckpointNotFound {
466 thread_id: thread_id.to_string(),
467 })?;
468
469 let mut data: CheckpointData<S> =
470 serde_json::from_slice(&bytes).map_err(|e| PeError::Storage {
471 details: format!("Checkpoint deserialization failed: {e}"),
472 })?;
473 data.checkpoint_id = Some(meta.id.clone());
475
476 match command {
477 Command::Resume { human_input } => {
478 data.phase_state
481 .set(&human_input)
482 .map_err(|e| PeError::Storage {
483 details: format!("Failed to store human input: {e}"),
484 })?;
485 }
486 Command::Goto { node } => {
487 if !self.graph.nodes.contains_key(&node) {
488 return Err(PeError::GraphValue {
489 details: format!("Goto target node '{}' does not exist", node),
490 });
491 }
492 data.next_nodes = vec![node];
493 }
494 Command::Update { update } => {
495 let typed_update: S::Update =
496 serde_json::from_value(update).map_err(|e| PeError::InvalidUpdate {
497 details: format!("Command::Update deserialization failed: {e}"),
498 })?;
499 data.state.apply(typed_update);
500 if let Some(ref interrupted) = data.interrupted_node {
503 let successors = self.graph.fixed_successors(interrupted);
504 if !successors.is_empty() {
505 data.next_nodes = successors;
506 }
507 }
508 }
509 }
510
511 Ok(data)
512 }
513
514 #[must_use = "the snapshot contains the state — inspect it"]
518 pub async fn get_state(&self, thread_id: &str) -> Result<Option<StateSnapshot<S>>, PeError> {
519 let Some(ref cp) = self.checkpointer else {
520 return Ok(None);
521 };
522
523 let Some((bytes, meta)) = cp.load_latest(thread_id).await? else {
524 return Ok(None);
525 };
526
527 deserialize_snapshot(bytes, meta)
528 }
529
530 #[must_use = "the history contains all past states"]
535 pub async fn get_state_history(
536 &self,
537 thread_id: &str,
538 ) -> Result<Vec<StateSnapshot<S>>, PeError> {
539 let Some(ref cp) = self.checkpointer else {
540 return Ok(Vec::new());
541 };
542
543 let metas = cp.list(thread_id).await?;
544 let mut snapshots = Vec::with_capacity(metas.len());
545
546 for meta in metas {
547 let Some(bytes) = cp.load_by_id(thread_id, &meta.id).await? else {
548 continue;
549 };
550 if let Ok(Some(snapshot)) = deserialize_snapshot(bytes, meta) {
551 snapshots.push(snapshot);
552 }
553 }
554
555 Ok(snapshots)
556 }
557}
558
559fn deserialize_snapshot<S: State>(
561 bytes: Vec<u8>,
562 meta: crate::checkpointer::CheckpointMeta,
563) -> Result<Option<StateSnapshot<S>>, PeError> {
564 let data: CheckpointData<S> = serde_json::from_slice(&bytes).map_err(|e| PeError::Storage {
565 details: format!("Checkpoint deserialization failed: {e}"),
566 })?;
567
568 Ok(Some(StateSnapshot {
569 state: data.state,
570 checkpoint_id: meta.id.clone(),
571 step: data.step,
572 thread_id: meta.thread_id,
573 parent_checkpoint_id: meta.parent_id.clone(),
574 created_at: meta.created_at, next_nodes: data.next_nodes,
576 }))
577}