Introduction
Agents forget why work happened.
They may leave behind chat transcripts, tool logs, test output, and Git diffs, but those artifacts rarely answer the question a maintainer asks later:
Why does this state exist?
yoagent-state is a small Rust continuity layer for long-running agents. It records durable state and lineage for agent work without replacing the agent loop, Git, the filesystem, CI, or a project database.
The full continuity chain starts from a goal:
goal -> task -> run -> observation -> failure -> hypothesis -> patch -> artifact -> eval -> decision -> promotion
flowchart LR
goal["goal"]
task["task"]
run["run"]
observation["observation"]
failure["failure"]
hypothesis["hypothesis"]
patch["patch"]
artifact["artifact"]
eval["eval"]
decision["decision"]
promoted["promoted status"]
task -- serves --> goal
run -- produces --> observation
observation -- observes --> failure
hypothesis -- explains --> failure
patch -- addresses --> failure
patch -- advances --> goal
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
decision -- allows --> promoted
That chain is the product. It tells you what the agent was trying to achieve, what work it started, what happened during the run, what failed, what the agent believed, what it proposed, what project artifact it referenced, what tested it, and what decision approved or rejected it.
It is a causal spine, not a required single linked list. Some runs start at a failure, some start at a goal, and some only record tool or model calls. The important part is that the graph can connect intent, execution, evidence, change, and decision.
A diff is usually one of those artifacts. The graph can also attach test logs, model transcripts, screenshots, benchmark output, review notes, or any other evidence an agent needs to explain the work later.
Promotion is represented as a patch status transition. The promotion should be backed by eval and decision lineage, not hidden inside a commit message.
What yoagent-state does
yoagent-state gives agents and humans a durable explanation layer:
- append-only events record what happened
- a graph projection turns events into queryable semantic state
- patches connect failures, evidence, artifacts, evals, and decisions
- lineage reports explain why a node exists
- JSONL persistence lets state survive process restart
The implementation is intentionally boring: Rust structs, JSON payloads, append-only storage, and an in-memory graph projection.
The boundary
Keep the boundary sharp:
Git stores what changed.
yoagent-state stores why it changed, what tested it, and what it means.
yoagent-state is not a replacement for Git, a workflow engine, or a graph database. It is the continuity layer that sits beside an agent loop and records the meaning of the work.
Who it is for
Use it if you are building:
- long-running agents
- agentic coding loops
- eval-driven project improvement systems
- tools that need patch/eval/decision lineage
yoagentoryoyo evolveintegrations
Start with the Quick Start to see the main lineage flow in under a minute.
Quick Start
This page gets you from a fresh clone to a working lineage report.
Prerequisites
You need a Rust toolchain with Cargo.
Check:
cargo --version
Clone and run the main demo
git clone https://github.com/yologdev/yoagent-state.git
cd yoagent-state
cargo run --example goal_lineage
You should see a lineage report like this:
# Make retry behavior reliable
- id: goal_retry_reliability
- kind: goal
- status: InProgress
## Incoming
- serves <- task_retry_timeout
- blocks <- failure_retry_timeout
- advances <- patch_retry_state
Read it as: goal_retry_reliability is being served by a task, blocked by a failure, and advanced by a patch.
flowchart LR
task["task_retry_timeout<br/>kind: task"]
failure["failure_retry_timeout<br/>kind: failure"]
patch["patch_retry_state<br/>kind: patch"]
goal["goal_retry_reliability<br/>kind: goal<br/>status: InProgress"]
task -- serves --> goal
failure -- blocks --> goal
patch -- advances --> goal
That is the core promise: state is not just a log. It is a graph that connects intent, work, evidence, change, and decision.
To inspect the patch/eval/decision lane directly:
cargo run --example patch_eval_decision
Run the test suite
cargo test
The tests cover event append and scan, state ops, replay, goal/task/failure lineage, typed packs, policy approvals, behavior subscriptions, fork/diff helpers, patch status transitions, lineage, JSONL persistence, and changed-file observer helpers.
Try local persistence
Initialize a local JSONL event log:
cargo run --bin yoagent-state -- init
Inspect the current graph:
cargo run --bin yoagent-state -- graph
Use a custom event log path:
YOAGENT_STATE_EVENTS=.yoyo/state/events.jsonl cargo run --bin yoagent-state -- events
The default local event log path is .yoagent-state/events.jsonl.
Read next
- Why Agents Need State
- First Lineage Example
- ActiveGraph-Inspired Runtime
- Patch, Eval, Decision Tutorial
Why Agents Need State
Logs tell you what happened. They do not reliably tell you why it mattered.
Long-running agents need a continuity layer because their work often spans many observations, tool calls, hypotheses, patches, evals, and decisions. Without durable state, the reasoning chain gets scattered across chat history, terminal output, temporary files, and Git commits.
The failure mode
An agent can make a good change and still leave behind a weak explanation:
test failed
agent tried something
files changed
test passed
commit created
That is not enough when someone later asks:
- What failure caused this patch?
- What evidence supported the hypothesis?
- Which eval validated it?
- What concrete diff did the patch refer to?
- Was the patch approved, rejected, promoted, or later made stale?
yoagent-state records that chain directly.
In the current runtime, that chain usually starts with durable intent:
goal -> task -> run -> observation -> failure -> hypothesis -> patch -> artifact -> eval -> decision -> promotion
The exact run may only use part of the graph, but the state model has a place for each piece.
Logs are not enough
Logs are chronological. Lineage is causal.
Chronology says:
tool ran, model responded, file changed, test passed
Lineage says:
patch_42 addresses failure_17
patch_42 references diff artifact patch_42.diff
eval_55 validated patch_42
decision_9 approved patch_42
flowchart LR
failure["failure_17"]
patch["patch_42"]
artifact["patch_42.diff"]
eval["eval_55"]
decision["decision_9"]
patch -- addresses --> failure
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
The second form is what agents and maintainers need to explain project evolution.
Durable state vs project diff
Git owns the concrete project state. yoagent-state owns the agent-facing meaning.
Git stores what changed.
yoagent-state stores why it changed, what tested it, and what it means.
This keeps the library small. It does not parse every symbol, mirror every file, or replace source control.
When to use it
Use yoagent-state when:
- an agent runs across multiple steps or sessions
- a patch needs evidence before promotion
- an eval result should be tied to a change
- a future agent should understand prior decisions
- project evolution needs an explainable history
Skip it when:
- the agent is stateless
- the task is a one-off script
- Git commit messages already capture enough context
- you need a full workflow engine, not a lineage layer
Core Mental Model
yoagent-state starts with three moving parts:
append events -> replay graph -> query lineage
flowchart LR
events["append-only events"]
replay["deterministic replay"]
graphNode["semantic graph projection"]
lineage["lineage queries"]
events --> replay --> graphNode --> lineage
The full runtime adds typed packs, behaviors, policies, replay, forks, frames, and views on top of that event-sourced base.
The graph is not the source of truth. It is a projection derived from append-only events.
Core graph shape
The current runtime is goal-centered. The common causal spine is:
goal -> task -> run -> observation -> failure -> hypothesis -> patch -> artifact -> eval -> decision -> promotion
flowchart LR
goal["goal"]
task["task"]
run["run"]
observation["observation"]
failure["failure"]
hypothesis["hypothesis"]
patch["patch"]
artifact["artifact"]
eval["eval"]
decision["decision"]
promoted["promoted status"]
task -- serves --> goal
run -- produced_by --> task
run -- produces --> observation
observation -- observes --> failure
failure -- blocks --> goal
hypothesis -- explains --> failure
patch -- addresses --> failure
patch -- advances --> goal
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
decision -- allows --> promoted
Read that as a graph shape, not a required pipeline:
goalcaptures the durable intent.taskis concrete work that serves a goal.run,model_call, andtool_callrecord execution.observation,failure, andhypothesispreserve what the agent noticed and believed.patchproposes a state or project change.artifactreferences concrete evidence such as diffs, logs, files, screenshots, or eval output.evalrecords validation.decisionrecords approval, rejection, or review state.- promotion is a
PatchStatus::Promotedtransition, not a separate graph node.
Side primitives such as policies, behaviors, packs, frames, forks, and views make this graph operational without changing the source-of-truth rule.
Event log
An event is an immutable fact about something that happened.
Examples:
run.startedtool.finishedgoal.createdtask.createdfailure.observedhypothesis.createdpatch.proposedpatch.status_changedartifact.attachedstate.ops_applied
Events are append-only. Do not mutate historical events.
State ops
State ops are the small mutation language for the graph projection.
They can:
- create or update nodes
- tombstone nodes
- create or delete relations
- mark nodes stale
- attach artifacts
Only state.ops_applied events mutate the graph directly.
Graph projection
The graph is a semantic view of agent state.
Common node kinds:
goaltaskrunobservationfailurehypothesispatchevaldecisionartifactfilemodel_calltool_callframe
Common relation kinds:
servesblocksadvancesobservesaddressesexplainsvalidated_byapproved_byrejected_bymodifiesreferencesproduced_bycontained_in_frameforked_from
The graph should stay lossy. It should preserve what matters for continuity and explanation, not every line of a log.
Patches
A state patch is a proposed semantic change with evidence.
It can include:
- base state version
- project reference
- preconditions
- expected effects
- evidence nodes
- artifact refs
- state ops
- lifecycle status
Patch lifecycle:
proposed -> applied_in_fork -> evaluated -> approved/rejected -> promoted
stateDiagram-v2
[*] --> Proposed
Proposed --> AppliedInFork
AppliedInFork --> Evaluated
Evaluated --> Approved
Evaluated --> Rejected
Approved --> Promoted
Proposed --> Stale
AppliedInFork --> Conflicted
Evaluated --> Stale
This lifecycle is one lane inside the larger goal-centered graph. A patch usually advances a goal, addresses a failure, references artifacts, is validated by evals, and is approved or rejected by decisions.
Artifacts
Artifacts point to external evidence such as:
- Git diffs
- commits
- files
- test output
- build logs
- eval result JSON
- model or tool output
Store paths, URIs, summaries, and hashes where practical.
Replay
On startup, the store scans events and replays them into the graph projection.
flowchart LR
store["EventStore"]
scan["scan events"]
projector["Projector"]
graphNode["Graph"]
store --> scan --> projector --> graphNode
This makes state durable without requiring a graph database.
Behaviors, policies, and packs
Typed packs validate object and relation shapes.
Behaviors react to event patterns and return state ops.
Policies gate sensitive actions by allowing, denying, or requiring approval.
flowchart TB
event["event"]
pack["typed pack validation"]
policy["policy gate"]
behavior["behavior subscription"]
ops["state ops"]
graphNode["graph projection"]
event --> pack --> policy
policy -- allow --> ops
policy -- require approval --> graphNode
event --> behavior --> ops
ops --> graphNode
These are runtime features, but they still preserve the same rule: durable state comes from append-only events.
ActiveGraph-Inspired Runtime
yoagent-state is inspired by Yohei Nakajima’s ActiveGraph work, adapted into an idiomatic Rust runtime for yoagent and yoyo evolve.
The full concept is:
append-only event log
-> deterministic replay
-> typed graph projection
-> pattern subscriptions
-> behaviors
-> policy-gated patches
-> replay, fork, and diff
flowchart LR
log["append-only event log"]
replay["deterministic replay"]
graphNode["typed graph projection"]
patterns["pattern subscriptions"]
behaviors["behaviors"]
policies["policy gates"]
forks["replay / fork / diff"]
log --> replay --> graphNode --> patterns --> behaviors
graphNode --> policies
graphNode --> forks
behaviors --> log
policies --> log
What changed in v0.2
The core lineage path now starts from goals:
goal -> task -> run -> observation -> failure -> hypothesis -> patch -> artifact -> eval -> decision -> promotion
flowchart LR
goal["goal"]
task["task"]
run["run"]
observation["observation"]
failure["failure"]
hypothesis["hypothesis"]
patch["patch"]
artifact["artifact"]
eval["eval"]
decision["decision"]
promoted["promoted status"]
task -- serves --> goal
run -- produces --> observation
observation -- observes --> failure
hypothesis -- explains --> failure
patch -- addresses --> failure
patch -- advances --> goal
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
decision -- allows --> promoted
This is the common graph spine. It is not a claim that every agent run must create every node. artifact includes diffs, logs, screenshots, files, eval output, and other external evidence. promotion is represented by the patch lifecycle status.
yoagent-state now has first-class IDs and helpers for:
- goals
- tasks
- runs
- observations
- hypotheses
- evals
- decisions
- project snapshots
- model calls
- tool calls
- frames
- forks
- behaviors
- policies
- packs
- views
Runtime layers
YoAgentState remains the simple state API: record events, apply ops, query graph, query lineage.
YoAgentRuntime adds the ActiveGraph-inspired runtime layer:
- register typed packs
- validate typed nodes and relations
- register behavior subscriptions
- enforce policy gates
- create approval requests
This keeps simple usage simple while allowing richer agent systems to use the full concept.
Extensible storage
The event log remains the source of truth.
Storage is split into traits:
EventStoreSnapshotStoreForkStoreIndexStoreArtifactStore
JSONL is implemented first because it is inspectable. SQLite, PostgreSQL, and graph-backed projections can be added later behind the same traits.
Behaviors
Behaviors subscribe to event patterns and return state ops.
They do not mutate the graph directly.
event -> matching behavior -> new events/state ops -> replayable state
flowchart LR
event["failure.observed"]
behavior["matching behavior"]
task["create investigation task"]
ops["state.ops_applied event"]
graphNode["replayed graph"]
event --> behavior --> task --> ops --> graphNode
This keeps behavior execution auditable.
Policies
Policies can allow, deny, or require approval for sensitive actions.
The current policy foundation supports approval requests for runtime operations. More policy surfaces can be added without changing the event-sourced model.
flowchart TB
action["runtime action"]
policy["policy check"]
allow["allow"]
deny["deny"]
approval["approval request node"]
action --> policy
policy --> allow
policy --> deny
policy --> approval
Replay, fork, diff
Replay rebuilds graph state from events.
Fork creates an alternate event history from a parent event cutoff.
Diff compares projected graphs so agents can inspect what changed between histories.
flowchart LR
events["events"]
cutoff["event cutoff"]
fork["fork graph"]
current["current graph"]
diff["graph diff"]
events --> cutoff --> fork
events --> current
fork --> diff
current --> diff
First Lineage Example
The smallest useful graph is a failure explained by a hypothesis.
Run:
cargo run --example basic_lineage
Expected shape:
# Attempt count is scoped to the cancelled future
- id: hypothesis_retry_state_lost
- kind: hypothesis
- status: unknown
## Outgoing
- explains -> failure_retry_timeout
What this demonstrates
The example creates two nodes:
- a
failurenode - a
hypothesisnode
Then it creates a relation:
hypothesis_retry_state_lost --explains--> failure_retry_timeout
flowchart LR
hypothesis["hypothesis_retry_state_lost<br/>kind: hypothesis"]
failure["failure_retry_timeout<br/>kind: failure"]
hypothesis -- explains --> failure
That edge is the start of lineage. Instead of storing a loose note in a transcript, the state layer records a queryable relationship.
Why it matters
Long-running agents need to preserve small facts like this. A later patch can address the failure, reference the hypothesis as evidence, and attach eval results.
The chain grows naturally:
goal -> task -> run -> observation -> failure -> hypothesis -> patch -> artifact -> eval -> decision -> promotion
flowchart LR
goal["goal"]
task["task"]
run["run"]
observation["observation"]
failure["failure"]
hypothesis["hypothesis"]
patch["patch"]
artifact["artifact"]
eval["eval"]
decision["decision"]
promoted["promoted status"]
task -- serves --> goal
run -- produces --> observation
observation -- observes --> failure
hypothesis -- explains --> failure
patch -- addresses --> failure
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
decision -- allows --> promoted
For this tiny example, only the hypothesis -> failure edge is created. The larger runtime can later connect that edge back to a goal and forward to a patch, artifacts, evals, and decisions.
Use this pattern whenever an agent observes something and forms a belief that should survive beyond the current run.
Patch, Eval, Decision Tutorial
This tutorial walks through the patch promotion lane:
failure -> patch -> diff artifact -> eval -> decision -> promotion
flowchart LR
failure["failure_17<br/>kind: failure"]
patch["patch_42<br/>kind: patch"]
artifact["patch_42.diff<br/>kind: git.diff artifact"]
eval["eval_55<br/>cargo test passed"]
decision["decision_9<br/>approved"]
promoted["PatchStatus::Promoted"]
patch -- addresses --> failure
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
decision -- allows --> promoted
This lane is important because it shows how a proposed change earns promotion: the patch addresses a failure, references a concrete artifact, is validated by an eval, and is approved by a decision.
It is not the whole yoagent-state model. It sits inside the larger goal-centered graph:
goal -> task -> run -> observation -> failure -> hypothesis -> patch -> artifact -> eval -> decision -> promotion
Run:
cargo run --example patch_eval_decision
Step 1: Record a failure
The example records a concrete failure:
failure_17: tool_retry_survives_timeout fails
The failure node explains what went wrong:
Retry state is lost when timeout cancels the future.
Step 2: Propose a patch
The patch is not just a title. It carries intent, evidence, preconditions, expected effects, and artifacts.
In the example:
- patch id:
patch_42 - title:
Persist retry state across timeout - evidence:
failure_17 - precondition:
tool_retry_survives_timeoutis still failing - expected effect:
tool_retry_survives_timeoutpasses
Step 3: Attach a diff artifact
Concrete project changes stay outside yoagent-state.
The patch references a fake Git diff artifact:
file://.yoyo/artifacts/patch_42.diff
This keeps the boundary clear:
Git stores what changed.
yoagent-state stores why it changed.
Step 4: Record an eval
The example records:
eval_55: cargo test tool_retry_survives_timeout passed
Then it creates the relation:
patch_42 --validated_by--> eval_55
Step 5: Record a decision
The example records a human approval decision:
decision_9: Eval passed; approve promotion
Then it creates:
patch_42 --approved_by--> decision_9
Step 6: Promote the patch
Finally, the patch status becomes Promoted.
The resulting lineage report shows:
patch_42
status: Promoted
addresses: failure_17
validated_by: eval_55
approved_by: decision_9
That is the point of this lane: a promoted patch is not just an event log entry. It has a durable explanation.
Examples
The examples move from small lineage to the current goal-centered runtime features.
Goal lineage
cargo run --example goal_lineage
Start here for the current core graph shape. It shows a goal being served by a task, blocked by a failure, and advanced by a patch.
flowchart LR
task["task_retry_timeout"]
failure["failure_retry_timeout"]
patch["patch_retry_state"]
goal["goal_retry_reliability"]
task -- serves --> goal
failure -- blocks --> goal
patch -- advances --> goal
Basic lineage
cargo run --example basic_lineage
Use this first if you want to understand nodes and relations.
It creates:
hypothesis_retry_state_lost --explains--> failure_retry_timeout
flowchart LR
hypothesis["hypothesis_retry_state_lost"]
failure["failure_retry_timeout"]
hypothesis -- explains --> failure
You should see a markdown lineage report with the hypothesis as the root and the failure as an outgoing relation.
Patch, eval, decision
cargo run --example patch_eval_decision
This is the main patch lifecycle demo.
It records:
- a failure
- a patch that addresses the failure
- a fake Git diff artifact
- an eval result
- an approval decision
- a promoted patch status
Use this pattern when an agent proposes a change and needs evidence before promotion.
yoagent integration
cargo run --example yoagent_integration
This uses YoAgentStateSink and YoAgentStateAdapter to record run, model, and tool lifecycle events.
It demonstrates how yoagent-state stays optional: the agent loop emits events to a sink, but the state layer does not take over execution.
yoyo evolve demo
cargo run --example yoyo_evolve_demo
This records a compact growth loop with:
- a failure
- a project reference
- a diff artifact
- changed file relations
- an eval result
- an approval decision
- a promoted patch status
Use this example when you want to see how project-level artifacts connect to semantic lineage.
Behavior subscription
cargo run --example behavior_subscription
This registers a behavior that reacts to failure.observed and creates an investigation task.
flowchart LR
failure["failure.observed"]
behavior["behavior subscription"]
task["investigation task"]
failure --> behavior --> task
Policy approval
cargo run --example policy_approval
This registers a policy requiring approval before node creation. The attempted operation is blocked and an approval request node is created.
flowchart LR
action["create node"]
policy["policy"]
request["approval request"]
action --> policy --> request
Replay and fork
cargo run --example replay_and_fork
This creates a graph, forks at an earlier event, and diffs the fork against current state.
flowchart LR
event["event cutoff"]
fork["fork graph"]
current["current graph"]
diff["graph diff"]
event --> fork
event --> current
fork --> diff
current --> diff
Typed pack
cargo run --example typed_pack
This registers a pack that validates goal, task, and serves relation shapes.
Choosing an example
Start here:
new to the current model -> goal_lineage
want the smallest relation -> basic_lineage
want the patch lifecycle -> patch_eval_decision
need behaviors -> behavior_subscription
need policy gates -> policy_approval
need replay/fork/diff -> replay_and_fork
need typed validation -> typed_pack
integrating an agent loop -> yoagent_integration
building yoyo-style project evolution -> yoyo_evolve_demo
Persistence Guide
yoagent-state ships with two v0 stores:
MemoryEventStoreJsonlEventStore
Both implement the same EventStore trait.
Memory store
Use memory storage for tests and examples:
#![allow(unused)]
fn main() {
let state = YoAgentState::load(MemoryEventStore::new()).await?;
}
Memory state disappears when the process exits.
JSONL store
Use JSONL when state should survive restart:
#![allow(unused)]
fn main() {
let state = YoAgentState::load(
JsonlEventStore::new(".yoagent-state/events.jsonl")
).await?;
}
The JSONL store writes one event per line. This makes state easy to inspect, copy, diff, and replay.
Load means replay
Loading state scans the event log and replays it into the graph:
events.jsonl -> replay -> graph projection
flowchart LR
jsonl["events.jsonl<br/>append-only"]
scan["scan"]
replay["replay"]
graphNode["in-memory graph projection"]
query["lineage / graph queries"]
jsonl --> scan --> replay --> graphNode --> query
The event log is durable. The graph is derived.
Why JSONL first
JSONL is boring and inspectable. That is useful while the state model is still young.
SQLite is intentionally left for later. It should be added after real usage proves the event shape and query needs.
Practical advice
- Store important artifacts outside the event log and reference them by URI.
- Hash important diffs, logs, and eval results when possible.
- Keep payloads useful but not huge.
- Treat the event log as append-only.
CLI Guide
The CLI is intentionally small. It exists to inspect local event logs and graph projections.
Commands
yoagent-state init
yoagent-state events
yoagent-state graph
yoagent-state node <id>
yoagent-state lineage <id>
yoagent-state lineage <id> --markdown
yoagent-state goal create <id> <title> [summary]
yoagent-state goal list
yoagent-state goal show <id>
yoagent-state goal status <id> <open|in-progress|satisfied|abandoned|blocked|stale>
yoagent-state patch list
yoagent-state patch show <id>
yoagent-state patch promote <id>
yoagent-state fork create <id> [event-id]
yoagent-state replay
When running from source, prefix commands with Cargo:
cargo run --bin yoagent-state -- graph
Event log path
The default event log is:
.yoagent-state/events.jsonl
Set YOAGENT_STATE_EVENTS to use another path:
YOAGENT_STATE_EVENTS=.yoyo/state/events.jsonl cargo run --bin yoagent-state -- events
Common local flow
Initialize:
cargo run --bin yoagent-state -- init
Inspect raw events:
cargo run --bin yoagent-state -- events
Inspect projected graph:
cargo run --bin yoagent-state -- graph
Print lineage:
cargo run --bin yoagent-state -- lineage patch_42 --markdown
List patches:
cargo run --bin yoagent-state -- patch list
Create a goal:
cargo run --bin yoagent-state -- goal create goal_retry "Make retry reliable"
Update goal status:
cargo run --bin yoagent-state -- goal status goal_retry in-progress
Create a fork at an event:
cargo run --bin yoagent-state -- fork create fork_before_patch event_123
Show one patch:
cargo run --bin yoagent-state -- patch show patch_42
Current limitation
The CLI is still intentionally small. Use the Rust API for full behavior/policy/pack flows and use the CLI for local inspection and simple goal/patch/fork operations.
API Guide
This page shows the common tasks you perform with yoagent-state.
The current API is organized around this goal-centered graph shape:
goal -> task -> run -> observation -> failure -> hypothesis -> patch -> artifact -> eval -> decision -> promotion
flowchart LR
goal["goal"]
task["task"]
failure["failure"]
hypothesis["hypothesis"]
patch["patch"]
artifact["artifact"]
eval["eval"]
decision["decision"]
task -- serves --> goal
failure -- blocks --> goal
hypothesis -- explains --> failure
patch -- addresses --> failure
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
patch -- advances --> goal
You can use only the pieces you need. The graph does not require every flow to create every node.
Create state in memory
Use memory state for tests, examples, and short-lived runs:
#![allow(unused)]
fn main() {
let state = YoAgentState::load(MemoryEventStore::new()).await?;
}
Create persisted state
Use JSONL when state should survive process restart:
#![allow(unused)]
fn main() {
let state = YoAgentState::load(
JsonlEventStore::new(".yoagent-state/events.jsonl")
).await?;
}
YoAgentState::load scans the event store and replays events into the graph projection.
Record a failure
#![allow(unused)]
fn main() {
state.record_failure(
ActorRef::agent("yoyo-evolve"),
NodeId::new("failure_17"),
"tool_retry_survives_timeout fails",
"Retry state is lost when timeout cancels the future.",
).await?;
}
Use failures for concrete observed problems, not vague concerns.
Record a goal
#![allow(unused)]
fn main() {
let goal = Goal::new(
GoalId::new("goal_retry_reliability"),
"Make retry behavior reliable",
"Retry attempts should survive timeout cancellation.",
ActorRef::agent("yoyo-evolve"),
);
state.record_goal(goal).await?;
}
Goals are the top of the common lineage graph.
Record a task
#![allow(unused)]
fn main() {
let task = Task {
id: TaskId::new("task_retry_timeout"),
title: "Fix timeout retry state".to_string(),
summary: "Investigate and patch retry state loss.".to_string(),
status: TaskStatus::InProgress,
goal: Some(GoalId::new("goal_retry_reliability")),
created_by: ActorRef::agent("yoyo-evolve"),
metadata: serde_json::json!({}),
};
state.record_task(task).await?;
}
Tasks link to goals with serves.
Record observations and hypotheses
#![allow(unused)]
fn main() {
let observation = Observation {
id: ObservationId::new("observation_retry_log"),
title: "Retry attempt reset observed".to_string(),
summary: "The second attempt starts from zero after timeout.".to_string(),
observed_in: None,
metadata: serde_json::json!({}),
};
state.record_observation(actor.clone(), observation).await?;
}
#![allow(unused)]
fn main() {
let hypothesis = Hypothesis {
id: HypothesisId::new("hypothesis_cancelled_future"),
title: "Attempt count is scoped to cancelled future".to_string(),
summary: "The retry state is dropped when the future is cancelled.".to_string(),
confidence: Some(0.8),
metadata: serde_json::json!({}),
};
state
.record_hypothesis(actor, hypothesis, Some(NodeId::new("failure_17")))
.await?;
}
Apply low-level state ops
Use apply_ops when you want direct control over nodes and relations:
#![allow(unused)]
fn main() {
state.apply_ops(
ActorRef::agent("demo"),
vec![StateOp::CreateNode {
id: NodeId::new("failure_1"),
kind: "failure".to_string(),
props: serde_json::json!({ "title": "retry failed" }),
}],
).await?;
}
This writes a state.ops_applied event and updates the graph projection.
Propose a patch
#![allow(unused)]
fn main() {
let mut patch = StatePatch::new(
PatchId::new("patch_42"),
"Persist retry state across timeout",
"Keep attempt count outside the cancelled future.",
ActorRef::agent("yoyo-evolve"),
);
patch.evidence.push(NodeId::new("failure_17"));
patch.expected_effects.push(ExpectedEffect::TestPasses {
name: "tool_retry_survives_timeout".to_string(),
});
state.propose_patch(patch).await?;
}
Patch evidence becomes lineage.
Attach an artifact
#![allow(unused)]
fn main() {
let artifact = ArtifactRef::new(
"git.diff",
"file://.yoyo/artifacts/patch_42.diff",
).with_summary("Fix retry persistence and add timeout regression test");
state.attach_artifact(NodeId::new("patch_42"), artifact).await?;
}
Artifacts should point to concrete external evidence such as diffs, commits, files, logs, or eval reports.
Record an eval result
#![allow(unused)]
fn main() {
state.record_eval_result(
ActorRef::agent("yoyo-evolve"),
NodeId::new("eval_55"),
PatchId::new("patch_42"),
"cargo test tool_retry_survives_timeout",
true,
).await?;
}
This creates an eval node and a validated_by relation from the patch to the eval.
Record a decision
#![allow(unused)]
fn main() {
state.record_decision(
ActorRef::user("yuanhao"),
NodeId::new("decision_9"),
PatchId::new("patch_42"),
true,
"Eval passed; approve promotion",
).await?;
}
Approved decisions create approved_by relations. Rejected decisions create rejected_by relations.
Update patch status
#![allow(unused)]
fn main() {
state.update_patch_status(
PatchId::new("patch_42"),
PatchStatus::Promoted,
Some("Promoted as commit def456".to_string()),
).await?;
}
Status is stored on the patch node and also recorded as a historical event.
Query lineage
#![allow(unused)]
fn main() {
let lineage = state.lineage(NodeId::new("patch_42")).await;
println!("{}", lineage.to_markdown());
}
Use lineage when you want to answer why a node exists and what it is connected to.
Query related entities
#![allow(unused)]
fn main() {
let patches = state.patches_for_failure(NodeId::new("failure_17")).await;
let evals = state.evals_for_patch(PatchId::new("patch_42")).await;
}
These helpers are intentionally narrow and practical.
Use the runtime layer
YoAgentRuntime adds typed packs, behaviors, and policies:
#![allow(unused)]
fn main() {
let state = YoAgentState::load(MemoryEventStore::new()).await?;
let mut runtime = YoAgentRuntime::new(state.clone());
}
Register a typed pack:
#![allow(unused)]
fn main() {
runtime.register_pack(
Pack::new(PackId::new("pack_lineage"), "lineage", "0.1.0")
.add_object_type(ObjectType::new("goal").require("title"))
.add_object_type(ObjectType::new("task").require("title"))
.add_relation_type(
RelationType::new("serves")
.from_kind("task")
.to_kind("goal"),
),
);
}
Register a policy:
#![allow(unused)]
fn main() {
runtime.register_policy(Policy::require_approval(
PolicyId::new("policy_create_node_review"),
"Creating graph nodes requires review",
PolicyAction::CreateNode,
));
}
Fork and diff:
#![allow(unused)]
fn main() {
let fork = state
.fork_at_event(ForkId::new("fork_before_task"), Some(event_id))
.await?;
let diff = diff_graphs(&fork.graph, &state.graph().await);
}
Concepts
Event
An event is an immutable fact about something that happened. Events are append-only JSON records with an ID, timestamp, actor, kind, payload, and optional causation or correlation metadata.
Common event kinds:
run.startedrun.finishedmodel.calledtool.finishedfailure.observedpatch.proposedpatch.status_changedartifact.attachedstate.ops_applied
Only state.ops_applied mutates the graph projection directly.
flowchart LR
event["event"]
ops["state.ops_applied"]
graphNode["graph projection"]
history["historical event only"]
event --> history
ops --> graphNode
Graph
The graph is a lossy semantic projection derived from events. It contains nodes, typed relations, artifacts, and a monotonically increasing version.
Keep the graph focused on agent continuity. Do not mirror every AST node, every log line, or every file.
flowchart LR
nodeA["node"]
relation["typed relation"]
nodeB["node"]
artifact["artifact reference"]
nodeA -- relation --> nodeB
nodeA -- references --> artifact
Node
A node is a semantic object such as a project, run, failure, hypothesis, patch, eval, decision, artifact, file, or task.
Relation
A relation is a typed edge between two nodes. Useful relation names include:
addressesexplainsvalidated_byapproved_byrejected_bymodifiesreferencesderived_from
Artifact
Artifacts reference external evidence: diffs, commits, logs, eval JSON, generated reports, model output, or tool output. Store paths, URIs, and hashes where possible.
Patch Lifecycle
A state patch records semantic intent and evidence. It is not a replacement for a Git diff.
The lifecycle is:
proposed -> applied_in_fork -> evaluated -> approved/rejected -> promoted
stateDiagram-v2
[*] --> Proposed
Proposed --> AppliedInFork
AppliedInFork --> Evaluated
Evaluated --> Approved
Evaluated --> Rejected
Approved --> Promoted
Proposed --> Stale
AppliedInFork --> Conflicted
Evaluated --> Stale
Rejected --> [*]
Promoted --> [*]
Additional states:
staleconflicted
The patch should answer:
- what failure it addresses
- what hypothesis or evidence supports it
- what concrete artifact contains the project diff
- what eval validated it
- who or what approved it
- which commit or promotion contains it
Promotion should require evidence such as a passing eval, a passing test, or explicit human approval.
flowchart LR
patch["patch"]
failure["failure"]
artifact["diff / log / file artifact"]
eval["passing eval or test"]
decision["approval decision"]
promoted["promoted status"]
patch -- addresses --> failure
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
decision --> promoted
yoagent Integration
yoagent-state should be optional. The agent loop emits events to a sink; the state layer records them and builds lineage.
The boundary is:
yoagent = execution
yoagent-state = state, lineage, patches, evals, decisions
flowchart LR
yoagent["yoagent<br/>execution loop"]
sink["YoAgentStateSink"]
adapter["YoAgentStateAdapter"]
events["append-only events"]
graphNode["semantic graph"]
yoagent --> sink --> adapter --> events --> graphNode
Adapter shape
The crate provides YoAgentStateSink and YoAgentStateAdapter.
The adapter records:
- run started and finished
- model called and finished
- tool called and finished
- failure observed when a tool finishes unsuccessfully
Minimal setup:
#![allow(unused)]
fn main() {
let state = YoAgentState::load(MemoryEventStore::new()).await?;
let sink = YoAgentStateAdapter::new(state, ActorRef::agent("yoagent"));
}
Run lifecycle
A typical run emits:
run.started
model.called
model.finished
tool.called
tool.finished
run.finished
sequenceDiagram
participant Run
participant Model
participant Tool
participant State
Run->>State: run.started
Model->>State: model.called
Model->>State: model.finished
Tool->>State: tool.called
Tool->>State: tool.finished
Run->>State: run.finished
Those events stay historical unless converted into state ops. This keeps the graph projection focused on durable semantic state.
Example
Run:
cargo run --example yoagent_integration
The example records a short run with model and tool events, then prints the event log as JSON.
Integration advice
- Keep state recording optional.
- Attach selected tool outputs as artifacts instead of dumping everything into graph nodes.
- Use causation and correlation IDs when connecting model/tool events to a run.
- Convert only meaningful facts into state ops.
The goal is continuity, not a heavier agent runtime.
yoyo evolve Integration
yoyo evolve is the first serious use case for yoagent-state.
It needs to grow a project while preserving why each change exists.
Growth loop
The intended loop is:
record goal
create task
observe project
record snapshot reference
run agent task or eval
observe failure
create hypothesis
propose patch
attach artifacts
apply patch in branch or worktree
run eval
record eval result
decide approve or reject
promote if approved
record lineage
flowchart LR
goal["goal"]
task["task"]
snapshot["project snapshot"]
failure["failure"]
hypothesis["hypothesis"]
patch["patch"]
artifact["diff artifact"]
eval["eval result"]
decision["decision"]
promoted["promoted patch"]
task -- serves --> goal
snapshot -- references --> task
failure -- blocks --> goal
hypothesis -- explains --> failure
patch -- addresses --> failure
patch -- advances --> goal
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
decision --> promoted
What yoagent-state tracks
For a project yoyo is improving, the state layer should track:
- why a module exists
- why a dependency was added
- what goal and task the change served
- what test validates behavior
- what failure caused a patch
- what decision approved it
- what assumptions became stale
- what version introduced behavior
Concrete project diffs remain external. Reference them with ArtifactRef and ProjectRef.
Demo
Run:
cargo run --example yoyo_evolve_demo
The demo records:
- a retry failure
- a base project reference
- a diff artifact
- changed file relations
- an eval result
- a human approval decision
- a promoted patch status
The resulting lineage includes:
patch_retry_timeout
addresses -> failure_retry_timeout
modifies -> file:crates/yoagent-runtime/src/tool.rs
modifies -> file:crates/yoagent-runtime/tests/retry.rs
validated_by -> eval_retry_timeout
approved_by -> decision_promote_retry_timeout
flowchart LR
patch["patch_retry_timeout"]
failure["failure_retry_timeout"]
source["file:crates/yoagent-runtime/src/tool.rs"]
test["file:crates/yoagent-runtime/tests/retry.rs"]
eval["eval_retry_timeout"]
decision["decision_promote_retry_timeout"]
patch -- addresses --> failure
patch -- modifies --> source
patch -- modifies --> test
patch -- validated_by --> eval
patch -- approved_by --> decision
Implementation rule
Do not hide mutation.
Every meaningful project change should have:
- a patch
- an artifact reference
- evidence or expected effect
- an eval or approval decision before promotion
That makes yoyo’s project growth inspectable instead of magical.
Safety Model
yoagent-state is designed around explicit mutation and evidence-backed promotion.
Self-modification must be explicit
Agents should not silently mutate prompts, policies, tools, memory, or code.
Use patches.
Promotion requires evidence
A patch should not be promoted without at least one of:
- passing eval
- passing test
- human approval
- policy approval
The evidence should be represented in lineage, not hidden in a transcript.
flowchart LR
patch["patch"]
evidence["eval / test / approval"]
decision["decision"]
promoted["promoted status"]
patch -- validated_by --> evidence
patch -- approved_by --> decision
evidence --> promoted
decision --> promoted
Project base must be checked
If a patch was created against commit abc123, do not blindly apply it to another commit.
Mark the patch stale or conflicted, then reobserve.
Staleness is first-class
Assumptions go stale. So do patches, projections, and observations.
Use stale nodes or statuses when state is no longer current.
Important artifacts need hashes
Use hashes for:
- diffs
- logs
- files
- eval outputs
- generated reports
Hashes make lineage more trustworthy.
Keep the layer small
Do not turn yoagent-state into a hidden automation engine. Behaviors and policies are allowed, but they should be explicit, registered, replayable, and represented in the graph. State and lineage come first.
For Agents
This page is for coding agents and LLMs working in this repo.
Read the root AGENTS.md first. This page mirrors the most important guidance in the hosted docs.
Project boundary
yoagent = execution
yoagent-state = state, lineage, patches, evals, decisions
yoyo evolve = growth loop using both
Do not turn this crate into a workflow engine, graph database, Git replacement, compiler, or universal memory system.
Where to look
src/event.rs: append-only event shapesrc/patch.rs: state ops, patches, statuses, preconditions, effectssrc/graph.rs: graph projection data structuressrc/projector.rs: event replay into graphsrc/state.rs: high-level public APIsrc/store.rs: memory and JSONL event storessrc/primitives.rs: goal/task/observation/hypothesis/eval/decision/frame typessrc/runtime.rs: typed packs, policies, approvals, and behaviorssrc/schema.rs: pack schemas and relation validationsrc/policy.rs: policy gates and approval requestssrc/behavior.rs: event-pattern behavior subscriptionssrc/fork.rs: replay fork and graph diff helpersexamples/: runnable usage flowstests/: regression coverage
Commands
cargo test
/Users/yuanhao/.cargo/bin/mdbook build docs
cargo run --example goal_lineage
cargo run --example patch_eval_decision
Design rule
Prefer boring, explicit state over clever machinery.
When adding behavior, preserve the goal-centered graph spine:
goal -> task -> run -> observation -> failure -> hypothesis -> patch -> artifact -> eval -> decision -> promotion
flowchart LR
task["task"]
failure["failure"]
patch["patch"]
goal["goal"]
artifact["artifact"]
eval["eval"]
decision["decision"]
task -- serves --> goal
failure -- blocks --> goal
patch -- advances --> goal
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
This is not a mandatory linear workflow. It is the common causal shape that lets a later agent answer what goal was being served, what happened, what changed, what evidence existed, and who or what approved the result.
When in doubt, store meaning and references. Let Git and the filesystem store concrete project state.
Roadmap
yoagent-state starts with an event-sourced graph runtime: append-only events, replayed graph projection, goal-first lineage, patch lifecycle, artifacts, policies, behaviors, forks, examples, and mdBook docs.
This roadmap lists the likely next steps without turning the project into a workflow engine, graph database, Git replacement, or universal agent framework.
Current Implementation
Implemented:
- Core ID, actor, event, artifact, patch, precondition, and expected-effect types.
MemoryEventStoreandJsonlEventStore.- Append-only event recording and replay into an in-memory graph.
- State operations for nodes, relations, stale markers, tombstones, and artifacts.
- Goal, task, run, observation, failure, hypothesis, patch, eval, decision, project snapshot, model call, tool call, frame, fork, behavior, policy, pack, and view IDs.
- Goal/task helpers, failure observation, hypothesis records, patch proposal, patch status changes, eval records, decision records, and lineage queries.
- Runtime layer for typed packs, policy gates, behavior subscriptions, replay, fork, and diff.
- Small
yoagentsink adapter for run/model/tool lifecycle events. - Coarse project observer helpers for changed files and diff artifacts.
- CLI for init, events, graph, node, lineage, goal create/list/show/status, patch list/show/promote, replay, and fork create.
- Examples and regression tests for the main state flows.
- mdBook user guide.
The current core graph shape is:
goal -> task -> run -> observation -> failure -> hypothesis -> patch -> artifact -> eval -> decision -> promotion
flowchart LR
goal["goal"]
task["task"]
run["run"]
observation["observation"]
failure["failure"]
hypothesis["hypothesis"]
patch["patch"]
artifact["artifact"]
eval["eval"]
decision["decision"]
promoted["promoted status"]
task -- serves --> goal
run -- produces --> observation
observation -- observes --> failure
hypothesis -- explains --> failure
patch -- addresses --> failure
patch -- advances --> goal
patch -- references --> artifact
patch -- validated_by --> eval
patch -- approved_by --> decision
decision --> promoted
This is a causal spine. It does not require every flow to create every node.
Phase 1: Runtime Hardening
Goal: make the current crate more reliable for early users.
- Add crate-level API examples as Rust doc tests.
- Add tests for tombstones, stale nodes, artifact attachment, relation deletion, and error cases.
- Add CLI integration tests using temporary JSONL event logs.
- Add schema compatibility tests for serialized event and patch JSON.
- Add stricter validation for patch status transitions.
- Add markdown lineage report tests so output remains stable.
- Improve error messages for malformed JSONL and missing nodes.
Success criteria:
- Public examples compile as doc tests.
- CLI behavior is covered by regression tests for goal, patch, replay, and fork commands.
- Event JSON compatibility is protected by fixtures.
Phase 2: Persistence and Replay
Goal: make local state durable and inspectable beyond simple demos.
- Make JSONL append safer for concurrent writers.
- Add compaction or snapshot support for large event logs.
- Add
scan_aftercoverage for missing IDs and resumed replay. - Add optional SQLite storage after JSONL behavior is proven.
- Add import/export commands for portable state bundles.
Success criteria:
- Restart/replay is reliable with larger logs.
- Users can choose JSONL for simplicity or SQLite for local scale.
Phase 3: Better Query and Reports
Goal: answer practical “why does this exist?” questions directly.
- Add focused query helpers:
- patches for failure
- evals for patch
- decisions for patch
- artifacts for node
- files modified by patch
- stale assumptions related to patch
- Expand markdown lineage reports with grouped sections:
- status
- addresses
- evidence
- evals
- modified files
- decisions
- promotion references
- Add JSON and markdown output options consistently to CLI commands.
Success criteria:
- One command can show why a patch exists, what validated it, what files changed, and whether it was promoted.
Phase 4: Project Observer v0+
Goal: connect semantic patches to concrete project diffs without becoming a compiler.
- Add git command helpers for changed files, base commit, and diff artifact creation.
- Hash important artifacts such as diffs, logs, and eval outputs.
- Detect common project changes:
- tests added or changed
- docs changed
- dependency manifest changed
- source files changed
- Add base commit precondition helpers.
- Mark patches stale or conflicted when the project base changes.
Success criteria:
- A patch can automatically reference changed files, base commit, diff artifact, and basic project facts.
Phase 5: yoagent Integration
Goal: let yoagent emit durable state without becoming state-heavy.
- Wire the adapter into real
yoagentrun hooks. - Record model/tool causation and correlation IDs.
- Attach selected tool outputs as artifacts.
- Add examples that use the actual
yoagentcrate once its integration point is stable. - Add tests for failed tool calls becoming failure observations.
Success criteria:
yoagentcan run normally with optional state recording enabled.
Phase 6: yoyo evolve Demo
Goal: prove the growth loop end to end.
- Create a demo command that:
- observes a failure
- proposes a patch
- records a diff artifact
- runs an eval command
- records eval output
- approves or rejects the patch
- prints a lineage report
- Use temporary branches or worktrees for patch evaluation.
- Keep promotion explicit and evidence-backed.
Success criteria:
- One demo shows goal -> task -> failure -> patch -> diff artifact -> eval -> decision -> promotion with a readable lineage report.
Phase 7: Policy and Safety Gates
Goal: make risky mutation explicit.
- Add policy checks for:
- prompt mutation
- tool configuration changes
- memory/state mutation
- project patch promotion
- Expand policy decisions as first-class decision nodes across more runtime actions.
- Require evidence before promotion.
- Add stale/conflicted status helpers for unsafe bases.
Success criteria:
- The library makes self-modification visible, reviewable, and auditable.
Later, Only If Needed
Potential extensions:
- More behavior subscription patterns beyond “failure observed -> create task”.
- Richer fork merge and promotion workflows.
- Richer project observers for Rust symbols, Cargo dependencies, and test surfaces.
- Web or TUI inspection UI.
- Remote artifact storage.
- Multi-agent views over the same state log.
These should wait until real yoyo evolve runs show that the added complexity is worth it.
Non-Goals to Preserve
Do not turn yoagent-state into:
- a replacement for Git
- a workflow engine
- a graph database platform
- a compiler or AST database
- a universal memory system
- a hidden self-modification mechanism
The guiding rule remains: simple but effective.
Acknowledgments
The core idea for yoagent-state comes from Yohei Nakajima and his ActiveGraph work.
This project is an independent Rust implementation inspired by that idea. It keeps the architecture small for yoagent and yoyo evolve, while preserving the important ActiveGraph-style primitives: append-only events, replayed graph projection, goals, tasks, observations, hypotheses, patches, artifacts, evals, decisions, policies, behaviors, packs, replay, and forks.
The guiding boundary is:
Git stores what changed.
yoagent-state stores why it changed, what tested it, and what it means.