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