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