1use crate::{
2 ActorRef, ArtifactRef, Decision, DecisionStatus, EvalResult, Event, EventId, EventStore,
3 ForkId, ForkSnapshot, Frame, Goal, GoalId, GoalStatus, Graph, GraphDiff, GraphSnapshot,
4 Hypothesis, Lineage, ModelCall, NodeId, Observation, PatchId, PatchStatus, ProjectSnapshot,
5 RunId, StateError, StateOp, StatePatch, Task, TaskId, TaskStatus, ToolCall, diff_graphs,
6 fork_events_at, project_event, replay,
7};
8use serde_json::json;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12#[derive(Debug)]
13pub struct YoAgentState<S: EventStore> {
14 store: Arc<S>,
15 graph: Arc<RwLock<Graph>>,
16 current_run: Arc<RwLock<Option<(RunId, EventId)>>>,
23}
24
25impl<S: EventStore> Clone for YoAgentState<S> {
26 fn clone(&self) -> Self {
27 Self {
28 store: self.store.clone(),
29 graph: self.graph.clone(),
30 current_run: self.current_run.clone(),
31 }
32 }
33}
34
35impl<S: EventStore> YoAgentState<S> {
36 pub async fn load(store: S) -> Result<Self, StateError> {
37 let events = store.scan().await?;
38 let graph = replay(&events)?;
39 Ok(Self {
40 store: Arc::new(store),
41 graph: Arc::new(RwLock::new(graph)),
42 current_run: Arc::new(RwLock::new(None)),
43 })
44 }
45
46 pub fn store(&self) -> Arc<S> {
47 self.store.clone()
48 }
49
50 pub async fn record_event(&self, mut event: Event) -> Result<EventId, StateError> {
60 if event.causation_id.is_none() || event.correlation_id.is_none() {
61 if let Some((run_id, run_event)) = self.current_run.read().await.clone() {
62 if event.causation_id.is_none() && event.kind != "run.started" {
63 event.causation_id = Some(run_event);
64 }
65 if event.correlation_id.is_none() {
66 event.correlation_id = Some(run_id.0);
67 }
68 }
69 }
70 let ids = self.store.append(vec![event.clone()]).await?;
71 {
72 let mut graph = self.graph.write().await;
73 project_event(&mut graph, &event)?;
74 }
75 Ok(ids[0].clone())
76 }
77
78 pub async fn apply_ops(
79 &self,
80 actor: ActorRef,
81 ops: Vec<StateOp>,
82 ) -> Result<EventId, StateError> {
83 self.apply_ops_caused_by(actor, ops, None).await
84 }
85
86 pub async fn apply_ops_caused_by(
91 &self,
92 actor: ActorRef,
93 ops: Vec<StateOp>,
94 caused_by: Option<EventId>,
95 ) -> Result<EventId, StateError> {
96 let mut event = Event::new(actor, crate::STATE_OPS_APPLIED, serde_json::to_value(ops)?);
97 event.causation_id = caused_by;
98 self.record_event(event).await
99 }
100
101 pub async fn propose_patch(&self, patch: StatePatch) -> Result<PatchId, StateError> {
102 let patch_id = patch.id.clone();
103 let actor = patch.created_by.clone();
104 let caused_by = self
105 .record_event(Event::new(
106 actor.clone(),
107 "patch.proposed",
108 serde_json::to_value(&patch)?,
109 ))
110 .await?;
111
112 let patch_node_id = NodeId::new(patch.id.0.clone());
113 let mut ops = vec![StateOp::CreateNode {
114 id: patch_node_id.clone(),
115 kind: "patch".to_string(),
116 props: json!({
117 "title": patch.title,
118 "summary": patch.summary,
119 "status": patch.status,
120 "base_state_version": patch.base_state_version,
121 "preconditions": patch.preconditions,
122 "expected_effects": patch.expected_effects,
123 "base_project_ref": patch.base_project_ref,
124 }),
125 }];
126
127 for evidence in patch.evidence {
128 ops.push(StateOp::CreateRelation {
129 from: patch_node_id.clone(),
130 rel: "addresses".to_string(),
131 to: evidence,
132 props: json!({}),
133 });
134 }
135
136 for artifact in patch.artifacts {
137 ops.push(StateOp::AttachArtifact {
138 id: patch_node_id.clone(),
139 artifact,
140 });
141 }
142
143 ops.extend(patch.ops);
144 self.apply_ops_caused_by(actor, ops, Some(caused_by))
145 .await?;
146 Ok(patch_id)
147 }
148
149 pub async fn update_patch_status(
150 &self,
151 patch_id: PatchId,
152 status: PatchStatus,
153 reason: Option<String>,
154 ) -> Result<EventId, StateError> {
155 let actor = ActorRef::system("yoagent-state");
156 let event = Event::new(
157 actor.clone(),
158 "patch.status_changed",
159 json!({
160 "patch_id": patch_id,
161 "status": status,
162 "reason": reason,
163 }),
164 );
165 let caused_by = self.record_event(event).await?;
166
167 self.apply_ops_caused_by(
168 actor,
169 vec![StateOp::UpdateNode {
170 id: NodeId::new(patch_id.0),
171 props: json!({
172 "status": status,
173 "status_reason": reason,
174 }),
175 }],
176 Some(caused_by),
177 )
178 .await
179 }
180
181 pub async fn attach_artifact(
182 &self,
183 node_id: NodeId,
184 artifact: ArtifactRef,
185 ) -> Result<EventId, StateError> {
186 let actor = ActorRef::system("yoagent-state");
187 let caused_by = self
188 .record_event(Event::new(
189 actor.clone(),
190 "artifact.attached",
191 json!({
192 "node_id": node_id,
193 "artifact": artifact,
194 }),
195 ))
196 .await?;
197
198 self.apply_ops_caused_by(
199 actor,
200 vec![StateOp::AttachArtifact {
201 id: node_id,
202 artifact,
203 }],
204 Some(caused_by),
205 )
206 .await
207 }
208
209 pub async fn graph(&self) -> GraphSnapshot {
210 self.graph.read().await.clone()
211 }
212
213 pub async fn get_node(&self, node_id: NodeId) -> Option<crate::Node> {
214 self.graph.read().await.get_node(&node_id).cloned()
215 }
216
217 pub async fn outgoing(&self, node_id: NodeId, rel: Option<&str>) -> Vec<crate::Relation> {
218 self.graph.read().await.outgoing(&node_id, rel)
219 }
220
221 pub async fn incoming(&self, node_id: NodeId, rel: Option<&str>) -> Vec<crate::Relation> {
222 self.graph.read().await.incoming(&node_id, rel)
223 }
224
225 pub async fn related(&self, node_id: NodeId) -> Vec<crate::Relation> {
226 self.graph.read().await.related(&node_id)
227 }
228
229 pub async fn patches_for_failure(&self, failure_id: NodeId) -> Vec<crate::Node> {
230 let graph = self.graph.read().await;
231 graph
232 .incoming(&failure_id, Some("addresses"))
233 .into_iter()
234 .filter_map(|rel| graph.get_node(&rel.from).cloned())
235 .filter(|node| node.kind == "patch")
236 .collect()
237 }
238
239 pub async fn evals_for_patch(&self, patch_id: PatchId) -> Vec<crate::Node> {
240 let patch_node_id = NodeId::new(patch_id.0);
241 let graph = self.graph.read().await;
242 graph
243 .outgoing(&patch_node_id, Some("validated_by"))
244 .into_iter()
245 .filter_map(|rel| graph.get_node(&rel.to).cloned())
246 .collect()
247 }
248
249 pub async fn lineage(&self, node_id: NodeId) -> Lineage {
250 let graph = self.graph.read().await;
251 Lineage::from_graph(&graph, &node_id)
252 }
253
254 pub async fn record_run_started(
255 &self,
256 actor: ActorRef,
257 run_id: RunId,
258 task: impl Into<String>,
259 ) -> Result<EventId, StateError> {
260 if let Some((open_run, _)) = self.current_run.read().await.as_ref() {
261 return Err(StateError::Validation(format!(
262 "run {open_run} is already open; finish it before starting {run_id}"
263 )));
264 }
265 let task = task.into();
266 let caused_by = self
267 .record_event(
268 Event::new(
269 actor.clone(),
270 "run.started",
271 json!({ "run_id": run_id, "task": task }),
272 )
273 .with_correlation(run_id.0.clone()),
274 )
275 .await?;
276 *self.current_run.write().await = Some((run_id.clone(), caused_by.clone()));
283 let result = self
284 .apply_ops_caused_by(
285 actor,
286 vec![StateOp::CreateNode {
287 id: NodeId::new(run_id.0),
288 kind: crate::KIND_RUN.to_string(),
289 props: json!({ "task": task, "status": "started" }),
290 }],
291 Some(caused_by),
292 )
293 .await;
294 if result.is_err() {
295 *self.current_run.write().await = None;
296 }
297 result
298 }
299
300 pub async fn record_run_finished(
301 &self,
302 actor: ActorRef,
303 run_id: RunId,
304 outcome: impl Into<String>,
305 ) -> Result<EventId, StateError> {
306 match self.current_run.read().await.as_ref() {
307 Some((open_run, _)) if *open_run == run_id => {}
308 Some((open_run, _)) => {
309 return Err(StateError::Validation(format!(
310 "cannot finish {run_id}: the open run is {open_run}"
311 )));
312 }
313 None => {
314 return Err(StateError::Validation(format!(
315 "cannot finish {run_id}: no run is open"
316 )));
317 }
318 }
319 let outcome = outcome.into();
320 let caused_by = self
324 .record_event(Event::new(
325 actor.clone(),
326 "run.finished",
327 json!({ "run_id": run_id, "outcome": outcome }),
328 ))
329 .await?;
330 let event_id = self
332 .apply_ops_caused_by(
333 actor,
334 vec![StateOp::UpdateNode {
335 id: NodeId::new(run_id.0.clone()),
336 props: json!({ "status": "finished", "outcome": outcome }),
337 }],
338 Some(caused_by),
339 )
340 .await?;
341 *self.current_run.write().await = None;
342 Ok(event_id)
343 }
344
345 pub async fn record_goal(&self, goal: Goal) -> Result<EventId, StateError> {
346 let actor = goal.owner.clone();
347 let caused_by = self
348 .record_event(Event::new(
349 actor.clone(),
350 "goal.created",
351 serde_json::to_value(&goal)?,
352 ))
353 .await?;
354 self.apply_ops_caused_by(
355 actor,
356 vec![StateOp::CreateNode {
357 id: NodeId::new(goal.id.0),
358 kind: crate::KIND_GOAL.to_string(),
359 props: json!({
360 "title": goal.title,
361 "summary": goal.summary,
362 "status": goal.status,
363 "owner": goal.owner,
364 "metadata": goal.metadata,
365 }),
366 }],
367 Some(caused_by),
368 )
369 .await
370 }
371
372 pub async fn update_goal_status(
373 &self,
374 goal_id: GoalId,
375 status: GoalStatus,
376 reason: Option<String>,
377 ) -> Result<EventId, StateError> {
378 let actor = ActorRef::system("yoagent-state");
379 let caused_by = self
380 .record_event(Event::new(
381 actor.clone(),
382 "goal.status_changed",
383 json!({ "goal_id": goal_id, "status": status, "reason": reason }),
384 ))
385 .await?;
386 self.apply_ops_caused_by(
387 actor,
388 vec![StateOp::UpdateNode {
389 id: NodeId::new(goal_id.0),
390 props: json!({ "status": status, "status_reason": reason }),
391 }],
392 Some(caused_by),
393 )
394 .await
395 }
396
397 pub async fn record_task(&self, task: Task) -> Result<EventId, StateError> {
398 let actor = task.created_by.clone();
399 let caused_by = self
400 .record_event(Event::new(
401 actor.clone(),
402 "task.created",
403 serde_json::to_value(&task)?,
404 ))
405 .await?;
406 let task_id = NodeId::new(task.id.0);
407 let mut ops = vec![StateOp::CreateNode {
408 id: task_id.clone(),
409 kind: crate::KIND_TASK.to_string(),
410 props: json!({
411 "title": task.title,
412 "summary": task.summary,
413 "status": task.status,
414 "metadata": task.metadata,
415 }),
416 }];
417 if let Some(goal) = task.goal {
418 ops.push(StateOp::CreateRelation {
419 from: task_id,
420 rel: crate::REL_SERVES.to_string(),
421 to: NodeId::new(goal.0),
422 props: json!({}),
423 });
424 }
425 self.apply_ops_caused_by(actor, ops, Some(caused_by)).await
426 }
427
428 pub async fn update_task_status(
429 &self,
430 task_id: TaskId,
431 status: TaskStatus,
432 reason: Option<String>,
433 ) -> Result<EventId, StateError> {
434 let actor = ActorRef::system("yoagent-state");
435 let caused_by = self
436 .record_event(Event::new(
437 actor.clone(),
438 "task.status_changed",
439 json!({ "task_id": task_id, "status": status, "reason": reason }),
440 ))
441 .await?;
442 self.apply_ops_caused_by(
443 actor,
444 vec![StateOp::UpdateNode {
445 id: NodeId::new(task_id.0),
446 props: json!({ "status": status, "status_reason": reason }),
447 }],
448 Some(caused_by),
449 )
450 .await
451 }
452
453 pub async fn record_observation(
454 &self,
455 actor: ActorRef,
456 observation: Observation,
457 ) -> Result<EventId, StateError> {
458 let caused_by = self
459 .record_event(Event::new(
460 actor.clone(),
461 "observation.created",
462 serde_json::to_value(&observation)?,
463 ))
464 .await?;
465 let observation_id = NodeId::new(observation.id.0);
466 let mut ops = vec![StateOp::CreateNode {
467 id: observation_id.clone(),
468 kind: crate::KIND_OBSERVATION.to_string(),
469 props: json!({
470 "title": observation.title,
471 "summary": observation.summary,
472 "metadata": observation.metadata,
473 }),
474 }];
475 if let Some(run_id) = observation.observed_in {
476 ops.push(StateOp::CreateRelation {
477 from: observation_id,
478 rel: crate::REL_OBSERVES.to_string(),
479 to: NodeId::new(run_id.0),
480 props: json!({}),
481 });
482 }
483 self.apply_ops_caused_by(actor, ops, Some(caused_by)).await
484 }
485
486 pub async fn record_hypothesis(
487 &self,
488 actor: ActorRef,
489 hypothesis: Hypothesis,
490 explains: Option<NodeId>,
491 ) -> Result<EventId, StateError> {
492 let caused_by = self
493 .record_event(Event::new(
494 actor.clone(),
495 "hypothesis.created",
496 serde_json::to_value(&hypothesis)?,
497 ))
498 .await?;
499 let hypothesis_id = NodeId::new(hypothesis.id.0);
500 let mut ops = vec![StateOp::CreateNode {
501 id: hypothesis_id.clone(),
502 kind: crate::KIND_HYPOTHESIS.to_string(),
503 props: json!({
504 "title": hypothesis.title,
505 "summary": hypothesis.summary,
506 "confidence": hypothesis.confidence,
507 "metadata": hypothesis.metadata,
508 }),
509 }];
510 if let Some(target) = explains {
511 ops.push(StateOp::CreateRelation {
512 from: hypothesis_id,
513 rel: crate::REL_EXPLAINS.to_string(),
514 to: target,
515 props: json!({}),
516 });
517 }
518 self.apply_ops_caused_by(actor, ops, Some(caused_by)).await
519 }
520
521 pub async fn record_failure(
522 &self,
523 actor: ActorRef,
524 failure_id: NodeId,
525 title: impl Into<String>,
526 summary: impl Into<String>,
527 ) -> Result<EventId, StateError> {
528 let title = title.into();
529 let summary = summary.into();
530 let caused_by = self
531 .record_event(Event::new(
532 actor.clone(),
533 "failure.observed",
534 json!({
535 "id": failure_id,
536 "failure_id": failure_id,
537 "title": title,
538 "summary": summary,
539 }),
540 ))
541 .await?;
542 self.apply_ops_caused_by(
543 actor,
544 vec![StateOp::CreateNode {
545 id: failure_id,
546 kind: "failure".to_string(),
547 props: json!({ "title": title, "summary": summary }),
548 }],
549 Some(caused_by),
550 )
551 .await
552 }
553
554 pub async fn record_eval_result(
555 &self,
556 actor: ActorRef,
557 eval_id: NodeId,
558 patch_id: PatchId,
559 command: impl Into<String>,
560 passed: bool,
561 ) -> Result<EventId, StateError> {
562 let patch_node_id = NodeId::new(patch_id.0);
563 self.apply_ops(
564 actor,
565 vec![
566 StateOp::CreateNode {
567 id: eval_id.clone(),
568 kind: "eval".to_string(),
569 props: json!({ "command": command.into(), "passed": passed }),
570 },
571 StateOp::CreateRelation {
572 from: patch_node_id,
573 rel: "validated_by".to_string(),
574 to: eval_id,
575 props: json!({ "passed": passed }),
576 },
577 ],
578 )
579 .await
580 }
581
582 pub async fn record_eval(
583 &self,
584 actor: ActorRef,
585 eval: EvalResult,
586 patch_id: Option<PatchId>,
587 ) -> Result<EventId, StateError> {
588 let caused_by = self
589 .record_event(Event::new(
590 actor.clone(),
591 "eval.finished",
592 serde_json::to_value(&eval)?,
593 ))
594 .await?;
595 let eval_node_id = NodeId::new(eval.id.0);
596 let mut ops = vec![StateOp::CreateNode {
597 id: eval_node_id.clone(),
598 kind: crate::KIND_EVAL.to_string(),
599 props: json!({
600 "command": eval.command,
601 "status": eval.status,
602 "score": eval.score,
603 "metadata": eval.metadata,
604 }),
605 }];
606 if let Some(patch_id) = patch_id {
607 ops.push(StateOp::CreateRelation {
608 from: NodeId::new(patch_id.0),
609 rel: crate::REL_VALIDATED_BY.to_string(),
610 to: eval_node_id,
611 props: json!({}),
612 });
613 }
614 self.apply_ops_caused_by(actor, ops, Some(caused_by)).await
615 }
616
617 pub async fn record_decision(
618 &self,
619 actor: ActorRef,
620 decision_id: NodeId,
621 patch_id: PatchId,
622 approved: bool,
623 reason: impl Into<String>,
624 ) -> Result<EventId, StateError> {
625 let patch_node_id = NodeId::new(patch_id.0);
626 let rel = if approved {
627 "approved_by"
628 } else {
629 "rejected_by"
630 };
631
632 self.apply_ops(
633 actor,
634 vec![
635 StateOp::CreateNode {
636 id: decision_id.clone(),
637 kind: "decision".to_string(),
638 props: json!({ "approved": approved, "reason": reason.into() }),
639 },
640 StateOp::CreateRelation {
641 from: patch_node_id,
642 rel: rel.to_string(),
643 to: decision_id,
644 props: json!({}),
645 },
646 ],
647 )
648 .await
649 }
650
651 pub async fn record_decision_node(
652 &self,
653 actor: ActorRef,
654 decision: Decision,
655 target: Option<NodeId>,
656 ) -> Result<EventId, StateError> {
657 let caused_by = self
658 .record_event(Event::new(
659 actor.clone(),
660 "decision.created",
661 serde_json::to_value(&decision)?,
662 ))
663 .await?;
664 let decision_node_id = NodeId::new(decision.id.0);
665 let mut ops = vec![StateOp::CreateNode {
666 id: decision_node_id.clone(),
667 kind: crate::KIND_DECISION.to_string(),
668 props: json!({
669 "status": decision.status,
670 "reason": decision.reason,
671 "decided_by": decision.decided_by,
672 "metadata": decision.metadata,
673 }),
674 }];
675 if let Some(target) = target {
676 let rel = match decision.status {
677 DecisionStatus::Approved => crate::REL_APPROVED_BY,
678 DecisionStatus::Rejected => crate::REL_REJECTED_BY,
679 _ => crate::REL_REFERENCES,
680 };
681 ops.push(StateOp::CreateRelation {
682 from: target,
683 rel: rel.to_string(),
684 to: decision_node_id,
685 props: json!({}),
686 });
687 }
688 self.apply_ops_caused_by(actor, ops, Some(caused_by)).await
689 }
690
691 pub async fn record_project_snapshot(
692 &self,
693 actor: ActorRef,
694 snapshot: ProjectSnapshot,
695 ) -> Result<EventId, StateError> {
696 let caused_by = self
697 .record_event(Event::new(
698 actor.clone(),
699 "project.snapshot_recorded",
700 serde_json::to_value(&snapshot)?,
701 ))
702 .await?;
703 let mut ops = vec![StateOp::CreateNode {
704 id: snapshot.id.clone(),
705 kind: crate::KIND_PROJECT_SNAPSHOT.to_string(),
706 props: json!({
707 "project": snapshot.project,
708 "metadata": snapshot.metadata,
709 }),
710 }];
711 for artifact in snapshot.artifacts {
712 ops.push(StateOp::AttachArtifact {
713 id: snapshot.id.clone(),
714 artifact,
715 });
716 }
717 self.apply_ops_caused_by(actor, ops, Some(caused_by)).await
718 }
719
720 pub async fn record_model_call(
721 &self,
722 actor: ActorRef,
723 call: ModelCall,
724 ) -> Result<EventId, StateError> {
725 let caused_by = self
726 .record_event(Event::new(
727 actor.clone(),
728 "model.called",
729 serde_json::to_value(&call)?,
730 ))
731 .await?;
732 self.apply_ops_caused_by(
733 actor,
734 vec![
735 StateOp::CreateNode {
736 id: call.id.clone(),
737 kind: crate::KIND_MODEL_CALL.to_string(),
738 props: json!({
739 "run_id": call.run_id,
740 "model": call.model,
741 "prompt_summary": call.prompt_summary,
742 "output_summary": call.output_summary,
743 "metadata": call.metadata,
744 }),
745 },
746 StateOp::CreateRelation {
747 from: call.id,
748 rel: crate::REL_PRODUCED_BY.to_string(),
749 to: NodeId::new(call.run_id.0),
750 props: json!({}),
751 },
752 ],
753 Some(caused_by),
754 )
755 .await
756 }
757
758 pub async fn record_tool_call(
759 &self,
760 actor: ActorRef,
761 call: ToolCall,
762 ) -> Result<EventId, StateError> {
763 let caused_by = self
764 .record_event(Event::new(
765 actor.clone(),
766 "tool.called",
767 serde_json::to_value(&call)?,
768 ))
769 .await?;
770 self.apply_ops_caused_by(
771 actor,
772 vec![
773 StateOp::CreateNode {
774 id: call.id.clone(),
775 kind: crate::KIND_TOOL_CALL.to_string(),
776 props: json!({
777 "run_id": call.run_id,
778 "tool": call.tool,
779 "input_summary": call.input_summary,
780 "output_summary": call.output_summary,
781 "success": call.success,
782 "metadata": call.metadata,
783 }),
784 },
785 StateOp::CreateRelation {
786 from: call.id,
787 rel: crate::REL_PRODUCED_BY.to_string(),
788 to: NodeId::new(call.run_id.0),
789 props: json!({}),
790 },
791 ],
792 Some(caused_by),
793 )
794 .await
795 }
796
797 pub async fn record_frame(&self, actor: ActorRef, frame: Frame) -> Result<EventId, StateError> {
798 let caused_by = self
799 .record_event(Event::new(
800 actor.clone(),
801 "frame.created",
802 serde_json::to_value(&frame)?,
803 ))
804 .await?;
805 let frame_node_id = NodeId::new(frame.id.0);
806 let mut ops = vec![StateOp::CreateNode {
807 id: frame_node_id.clone(),
808 kind: crate::KIND_FRAME.to_string(),
809 props: json!({
810 "title": frame.title,
811 "summary": frame.summary,
812 "metadata": frame.metadata,
813 }),
814 }];
815 if let Some(parent) = frame.parent {
816 ops.push(StateOp::CreateRelation {
817 from: frame_node_id,
818 rel: crate::REL_CONTAINED_IN_FRAME.to_string(),
819 to: NodeId::new(parent.0),
820 props: json!({}),
821 });
822 }
823 self.apply_ops_caused_by(actor, ops, Some(caused_by)).await
824 }
825
826 pub async fn link(
827 &self,
828 actor: ActorRef,
829 from: NodeId,
830 rel: impl Into<String>,
831 to: NodeId,
832 ) -> Result<EventId, StateError> {
833 self.apply_ops(
834 actor,
835 vec![StateOp::CreateRelation {
836 from,
837 rel: rel.into(),
838 to,
839 props: json!({}),
840 }],
841 )
842 .await
843 }
844
845 pub async fn fork_at_event(
846 &self,
847 fork_id: ForkId,
848 parent_event: Option<EventId>,
849 ) -> Result<ForkSnapshot, StateError> {
850 let events = self.store.scan().await?;
851 fork_events_at(&events, fork_id, parent_event)
852 }
853
854 pub async fn diff_with(&self, other: &Graph) -> GraphDiff {
855 let current = self.graph().await;
856 diff_graphs(¤t, other)
857 }
858}