GASP (Git Agent State Protocol) is a standard for portable agent state, native to git. Throughout this document, "GASP" and "the protocol" both refer to the normative standard defined in Part I. The name states the substrate: an agent's durable state lives in a git repository — an append-only event log that folds into a typed graph of goals, patches, evals, and decisions, and that graph, not a flat transcript, is what makes it agent state.
Status: buildable.
Part I is the normative protocol — no yoyo, no library, no language. Part II is the reference runtime (yoagent-state, Rust). Part III is the reference adapters for wrapping closed agents (Claude Code, Codex, and any other). Part IV is the reference agent (yoyo). Part V is honest caveats. Part VI is the conformance kit (fixture + checker).
The one-sentence idea: an agent is stateless; its durable state is an append-only event log that folds into a queryable graph, committed into a git repo, and that repo — not the model or the runtime — is the agent. Point any GASP-conformant runtime at the repo URL and the same agent resumes anywhere, on any model.
An agent has two parts: an executor (model + loop + tools) and its state. GASP governs only state. Swap the executor freely; the state is independent of it.
State spans two media inside one repo:
- Git owns the concrete what — code,
identity/,skills/, commits, diffs. Lossless, bisectable, shippable. - An append-only event log owns the semantic why — a stream of typed events (goals, tasks, runs, observations, tool calls, failures, patches, evals, decisions) committed into the same repo at
state/events.jsonl. Folded on load, it reconstructs a queryable history: what the agent was trying to do, why each change exists, what tested it, what was decided.
The repo is the agent. The log makes its history a queryable graph. Git stores and ships it. The executor reads a projection of the log, acts, and appends new events. Events reference git artifacts (a patch points to a commit as its artifact); git stores the artifacts. Neither duplicates the other.
Five rules. Satisfy them and any other conformant runtime can resume your agent. None names a language or a library — that is what makes GASP a standard and not an API.
- State lives in a git repo at the layout below — log, identity, skills, projections.
- The event log is append-only, committed, and uses the event vocabulary below.
- Identity and skills are committed artifacts, loaded at runtime, never compiled in.
- Restore is
clone + replay— fold the committed log into state; load identity and skills. - The executor is swappable; state is independent of it — swapping model or runtime touches
.agent/config.toml, nothing else.
agent-repo/
├── AGENT.md # Manifest: spec version, agent id, identity hash, executor binding
├── identity/ # FIRST-CLASS, human-gated constitution
│ ├── IDENTITY.md # Who the agent is, goals, hard rules (immutable-by-policy)
│ ├── PERSONALITY.md # Voice, values, tone (immutable-by-policy)
│ └── PRINCIPLES.md # Operating principles / constraints
├── skills/ # FIRST-CLASS, versioned capabilities
│ └── <skill-name>/
│ ├── SKILL.md # YAML frontmatter (name, description, tools) + body
│ ├── scripts/
│ └── references/
├── state/
│ └── events.jsonl # SOURCE OF TRUTH — append-only semantic event log
├── memory/ # derived memory layer (see "Memory" below)
│ ├── facts.jsonl # append-only DISTILLED facts — never a history mirror
│ └── active_memory.md # regenerated synthesis injected into context
├── journal/ # human-readable narrative — a projection of run events
│ └── JOURNAL.md
├── transcripts/ # COLD archive, out of the hot path — optional, prunable
│ └── <run-id>.jsonl.gz # raw turn-by-turn events, referenced by hash from a run event
├── snapshots/ # PROJECTION checkpoints for fast restore (optional)
│ └── <event-id>/graph.json # materialized projection at a log position (+ integrity record)
└── .agent/ # control plane
├── HEAD # last applied event id (reserved; LOCAL-ONLY, gitignored)
├── config.toml # executor binding: model, provider (COMMITTED — restore reads it)
├── packs/ # optional vocabulary packs, *.json (COMMITTED — check 3 reads them)
└── lease # active-worker lease (LOCAL-ONLY, gitignored)
What's first-class vs derived — four tiers, not two:
- First-class, committed: the event log,
identity/,skills/. The log is append-only; identity is human-gated; skills are versioned (each change a commit). - Derived, append-only:
memory/facts.jsonl. Extracted from runs by a synthesis step (usually an LLM), so it is not a deterministic fold and not freely regenerable — it is committed and append-only like the log, but derived. See "Memory" below. - Projections, regenerable:
memory/active_memory.md,journal/,snapshots/. Deterministic (or freely re-runnable) functions of the log and facts. Free to overwrite — with one exception:JOURNAL.mditself is append-only (commit rule 1, check 4), so the journal is regenerated by appending new run entries, never by rewriting existing lines. - Cold, prunable:
transcripts/. Never the source of truth.
.agent/ is the control plane: config.toml is committed (restore step 2 reads it); HEAD and lease are machine-local coordination state and MUST be gitignored — a lease in history is meaningless on clone and invites push races.
state/events.jsonl — append-only, one JSON object per line. Each line is an event envelope:
| field | meaning |
|---|---|
id |
unique event id (event_<uuid>) |
schema_version |
event schema version (currently 1) |
ts_ms |
unix epoch milliseconds |
actor |
who caused it — { "kind": "agent|user|system|tool", "id": "..." } |
kind |
the event type (dotted; see below) |
payload |
JSON; shape depends on kind |
causation_id |
the event that caused this one (null at a root) — the lineage link |
correlation_id |
groups events of one unit of work (e.g. a run) |
Ordering (normative). The physical line order of state/events.jsonl is the authoritative total order of events. ts_ms is advisory (clock skew, backfilled adapters); replay determinism (check 2) is defined over line order, never over timestamps.
Two event families share the log:
- Domain events — the semantic record.
kindisentity.verb:goal.created,goal.status_changed,task.created,task.status_changed,run.started,run.finished,observation.created,failure.observed,hypothesis.created,patch.proposed,patch.status_changed,eval.finished,decision.created,model.called,model.finished,tool.called,tool.finished,artifact.attached,frame.created,project.snapshot_recorded. Thepayloadis the entity. - State-ops events —
kindisstate.ops_applied,payloadis an array of graph mutations. This is the only family the reducer folds into the graph; domain events are a parallel human-and-machine-readable audit layer. The ops are:CreateNode {id, kind, props},UpdateNode {id, props}(deep-merge),TombstoneNode {id, reason},CreateRelation {from, rel, to, props},DeleteRelation {from, rel, to},MarkStale {id, reason},AttachArtifact {id, artifact}. They serialize externally-tagged:{"CreateNode": { ... }}.
Pairing rule (normative). An entity-creating domain event — goal.created, task.created, observation.created, failure.observed, hypothesis.created, patch.proposed, eval.finished, decision.created, frame.created — is followed by exactly one state.ops_applied event whose causation_id is the domain event's id and whose ops create the entity. The ops MUST be consistent with the domain event's payload — the entity's id, kind, and status are mechanically checked (check 7); richer ops in the same pair (extra relations, artifact attachments) are permitted so long as they do not contradict it. Raw-layer kinds (run.started, model.called, tool.called, …) and *.status_changed MAY pair the same way but are not required to. An ops event's causation_id must name a domain event, never another ops event. A state.ops_applied with a null causation (an ops-only maintenance mutation) is permitted but must not restate a domain entity. This is what keeps the audit layer and the folded graph from silently diverging.
Node kinds that CreateNode uses: goal, task, run, observation, failure, hypothesis, patch, eval, decision, model_call, tool_call, frame, project_snapshot, plus the governance types policy and behavior. (Artifacts attach to nodes via AttachArtifact.)
Relation kinds that CreateRelation uses: serves, blocks, advances, observes, explains, addresses, modifies, validated_by, approved_by, rejected_by, produced_by, derived_from, depends_on, supersedes, contained_in_frame, forked_from, references.
The common causal spine (a backbone, not a forced linear workflow):
goal -> task -> run -> observation -> failure -> hypothesis -> patch -> artifact -> eval -> decision -> promotion
A "session," in human terms, is a run (one execution) under a task under a goal. Each model.called records the model, so a model swap is fully auditable from the log alone. The vocabulary is open and pack-validated — a runtime may declare additional kinds in a Pack; the kinds above are the shared baseline every conformant runtime understands.
A complete run where a skill patch is tested and promoted. Ids are shortened for readability (the runtime generates event_<uuid>, goal_<uuid>, …). Each entity-creating domain event is followed by the state.ops_applied event that mutates the graph (note each ops event's causation_id is its domain event — the pairing rule); raw-layer events (run.started, tool.called) go unpaired here, as the rule permits. Only ops events fold into state.
{"id":"event_01","schema_version":1,"ts_ms":1739000000000,"actor":{"kind":"agent","id":"evolve"},"kind":"goal.created","payload":{"id":"goal_retry","title":"Make retry reliable","summary":"retries drop state after timeout","status":"Open","owner":{"kind":"agent","id":"evolve"},"metadata":{}},"causation_id":null,"correlation_id":"goal_retry"}
{"id":"event_02","schema_version":1,"ts_ms":1739000000001,"actor":{"kind":"agent","id":"evolve"},"kind":"state.ops_applied","payload":[{"CreateNode":{"id":"goal_retry","kind":"goal","props":{"title":"Make retry reliable","status":"Open"}}}],"causation_id":"event_01","correlation_id":"goal_retry"}
{"id":"event_03","schema_version":1,"ts_ms":1739000000100,"actor":{"kind":"agent","id":"evolve"},"kind":"run.started","payload":{"run_id":"run_42","task":"fix retry skill","metadata":{}},"causation_id":"event_01","correlation_id":"run_42"}
{"id":"event_04","schema_version":1,"ts_ms":1739000000200,"actor":{"kind":"agent","id":"evolve"},"kind":"patch.proposed","payload":{"id":"patch_9","title":"persist retry counter","status":"Proposed","created_by":{"kind":"agent","id":"evolve"}},"causation_id":"event_03","correlation_id":"run_42"}
{"id":"event_05","schema_version":1,"ts_ms":1739000000201,"actor":{"kind":"agent","id":"evolve"},"kind":"state.ops_applied","payload":[{"CreateNode":{"id":"patch_9","kind":"patch","props":{"title":"persist retry counter","status":"Proposed","references_commit":"abc1234"}}},{"CreateRelation":{"from":"patch_9","rel":"advances","to":"goal_retry","props":{}}}],"causation_id":"event_04","correlation_id":"run_42"}
{"id":"event_06","schema_version":1,"ts_ms":1739000000300,"actor":{"kind":"tool","id":"cargo"},"kind":"tool.called","payload":{"id":"tool_1","run_id":"run_42","tool":"cargo test","input_summary":"cargo test retry","output_summary":"ok, 1 passed","success":true,"metadata":{}},"causation_id":"event_04","correlation_id":"run_42"}
{"id":"event_07","schema_version":1,"ts_ms":1739000000400,"actor":{"kind":"agent","id":"evolve"},"kind":"eval.finished","payload":{"id":"eval_5","command":"cargo test retry","status":"Passed","score":1.0,"metadata":{}},"causation_id":"event_06","correlation_id":"run_42"}
{"id":"event_08","schema_version":1,"ts_ms":1739000000401,"actor":{"kind":"agent","id":"evolve"},"kind":"state.ops_applied","payload":[{"CreateNode":{"id":"eval_5","kind":"eval","props":{"command":"cargo test retry","status":"Passed","score":1.0}}},{"CreateRelation":{"from":"patch_9","rel":"validated_by","to":"eval_5","props":{}}}],"causation_id":"event_07","correlation_id":"run_42"}
{"id":"event_09","schema_version":1,"ts_ms":1739000000500,"actor":{"kind":"agent","id":"evolve"},"kind":"decision.created","payload":{"id":"decision_3","status":"Approved","reason":"eval passed; promote","decided_by":{"kind":"agent","id":"evolve"},"metadata":{}},"causation_id":"event_07","correlation_id":"run_42"}
{"id":"event_10","schema_version":1,"ts_ms":1739000000501,"actor":{"kind":"agent","id":"evolve"},"kind":"state.ops_applied","payload":[{"CreateNode":{"id":"decision_3","kind":"decision","props":{"status":"Approved"}}},{"CreateRelation":{"from":"patch_9","rel":"approved_by","to":"decision_3","props":{}}},{"UpdateNode":{"id":"patch_9","props":{"status":"Promoted"}}}],"causation_id":"event_09","correlation_id":"run_42"}Fold the ten lines and the graph holds: goal_retry advanced by patch_9 (status Promoted, references_commit abc1234), the patch validated_by a passed eval_5 and approved_by decision_3. That subgraph is the version's track record — copy these shapes to write a first emitter or a transcript projector.
memory/ is where the agent keeps what it learned, not what it did. What it did is state/events.jsonl, completely and exclusively — facts.jsonl MUST NOT mirror the work history, or it becomes an unbounded second log that defeats its purpose.
Admission criterion. A fact earns a line in facts.jsonl only if it is (a) useful without its originating context — it changes future behavior rather than recording past behavior — and (b) not derivable from a lineage query over the fold. "The flaky test is retry_timeout; ignore single failures", "this API rate-limits at 10 rps", "the maintainer prefers squash merges" belong. "Ran cargo test at 14:32, passed" never does — that is a tool.called/eval.finished event.
Format. One JSON object per line, append-only: {"id": "fact_<uuid>", "ts_ms": ..., "text": "...", "derived_from": ["event_..." | "run_..."], "supersedes": "fact_..." | null}. derived_from keeps every fact auditable back to the events that taught it. Superseding a stale fact appends a new line pointing at the old one — append-only compaction; the synthesis step that writes active_memory.md uses only the latest version of each fact chain.
Regenerability. Fact extraction is a synthesis step (usually an LLM), not a deterministic fold — so facts.jsonl is committed and append-only like the log, and only active_memory.md (the current synthesis of live facts) is the freely-regenerable projection. Version the synthesis prompt; a bad synthesis can distort the human-facing story but never destroys facts.
YAML frontmatter with required name and description (plus optional tools/allowed-tools) and a markdown body. Progressive disclosure: frontmatter loaded at startup, body on demand. Compatible with the open Agent Skills convention.
How a self-modifying agent avoids silently rotting, expressed entirely in the vocabulary above so any conformant runtime reads it the same way:
- A skill change is a
patchthatadvancesa goal (andaddressesa failure if it fixes one). The patch pins the skill commit — as a prop or an attached artifact — which fixes the record to a skill version. - The verification gate (tests, an oracle) produces an
eval:patch --validated_by--> eval. - Promote or revert is a
decision: the patch isapproved_by(orrejected_by) the decision, and the patch'sstatusis set toPromoted(or reverted) via anUpdateNode. Promotion is a status on the patch, not a separate node. - A version's track record (pass rate, sample size, trend) is a fold over its patches + evals + decisions — data that travels with the repo, so a swapped executor knows how good the agent's own skills are.
The boundary: the eval and decision facts live in the log (part of GASP); the promotion policy — minimum sample size, win-margin to replace an incumbent — is the executor's, never GASP's. The oracle is a plug-in; the policy that consumes its evals stays out of the log format.
gasp restore <git-url> [--at <event-id>] [--model <model>]
git clone <url>(optionally a partial clone to skip cold transcripts).- Read
AGENT.md+.agent/config.toml; verify spec version and identity hash. - Load
identity/andskills/directly (runtime, never compiled). - Fold the committed
state/events.jsonlinto state (optionally seeded from the latestsnapshots/checkpoint at or before--at, then fold forward). This reconstructs open goals, patch lifecycle, lineage. - Bind the chosen model/provider as the executor.
- Resume: take the lease, continue the loop.
Snapshot integrity (normative). A snapshot at snapshots/<event-id>/graph.json is a JSON object {"graph": <the folded graph>, "integrity": {"event_id": "...", "line_count": N, "sha256": "..."}} where line_count is the number of physical lines of state/events.jsonl the snapshot folds through, event_id is the id of the last event in that prefix, and sha256 is the SHA-256 of the raw bytes of those line_count lines, including their newlines. Restore verifies the record against the cloned log before seeding; on mismatch (rebased history, sharded log) it falls back to a full fold. A snapshot is an optimization, never an authority.
"Same agent, everywhere" = the same clone + fold on any machine. "Knows about you" = identity/, memory/, and the log's lineage all travel with the repo.
GASP governs state, not your executor. Keep your model, loop, prompts, and tools; route their state through GASP. Three additive steps — closer to adding OpenTelemetry than adopting a framework:
- Point at a repo. On start, clone-or-init the state repo from a URL. One line of config; this is the agent's identity + memory + log.
- Instrument the transitions. Your loop already has the moments that matter — it picked a goal, ran a tool, a test passed, a change landed. Emit an event at each, in the vocabulary. The only adapter you write is the map from your internal concepts (your "task" →
task, your test gate →eval, your accepted change →patch+decision). - Load on start, commit on boundary. Fold the log at startup, run your loop, persist at run close.
Everything else stays yours. You don't rewrite the agent; you instrument its state.
A conformant loop, stateless over the committed log:
take_lease()
state = fold(state/events.jsonl) # reconstruct projection from the log
start_run(goal) # append goal / task / run events
while not run_done:
decision = model.next(view(state)) # PURE: state in, decision out (model swappable)
if decision.is_tool_call:
result = run_tool(decision) # the gate (tests/oracle) yields an `eval`
append(events_from(result)) # tool_call / observation / eval
close_run(): # persist
append(patch / decision) # promote or revert per executor policy
regenerate(journal, active_memory) # projections
commit(state/events.jsonl + projections + code) with Run-Id / Outcome trailer
release_lease()
The model is a pure function model.next(view) -> decision; it holds no durable state. Swapping the model touches .agent/config.toml only — log, identity, and skills are untouched, and each model_call event records which model produced it.
- The log is committed, append-only. Structural immutability is enforced by the conformance checker walking git history (check 4): any commit that modifies or deletes existing lines of
state/events.jsonl,*/facts.jsonl, orJOURNAL.mdis non-conformant. A local pre-commit hook that rejects such diffs is recommended as a fast guard — but hooks do not clone with the repo, so the hook is advisory; the history walk is the gate. - One run = one closing commit (code work commits freely inside it — don't squash away bisectable history). The closing commit carries a trailer for in-git orientation (the log fold, not the trailer, is the real query surface):
Run-Id: 01J... Goal: goal_retry_reliability Outcome: promoted - Projections are free to rewrite — they are deterministic folds of the log. (
facts.jsonlis not a projection — it is append-only; see Memory.) - Identity changes are human-gated and follow a defined procedure: one commit, made or approved by a human, that (a) edits
identity/, (b) updates the identity hash inAGENT.md, and (c) appends adecisionevent recording who approved the change and why. All three in the same commit, so the hash in the manifest never disagrees withidentity/at any point in history — an identity edit that skips the hash update would otherwise permanently fail restore step 2. The identity hash is SHA-256 over eachidentity/file's relative path followed by a newline and its bytes, in byte-order-sorted path order:find identity -type f | LC_ALL=C sort | while IFS= read -r f; do printf '%s\n' "$f"; cat "$f"; done | shasum -a 256 - Skills are versioned commits, one change per commit, so each version is a pinnable hash a
patchreferences as itsartifact. - Single-writer lease before appending (
.agent/lease, worker id + TTL, local-only and gitignored) prevents concurrent-writer corruption. Multiplayer and A/B use branches, not concurrent writes to one ref. - Snapshots on cadence (
snapshots/<event-id>/graph.json+ integrity record) so restore folds from a checkpoint instead of all of history.
Conformance is always an adapter that translates an agent's native record into the GASP vocabulary. The only variable is placement. For agents you build, the adapter is inline — the emit calls of the integration contract, inside your loop. For agents you don't control (Claude Code, Codex, and others), the adapter sits outside the loop and translates their output; you never touch their internals, because GASP is defined over state, not the loop.
Fidelity depends on what the agent exposes — three rungs, best to worst:
- Native hooks / session-store — the agent fires callbacks at its own boundaries; emit GASP events in-band as it runs. Real-time, highest fidelity, and (where hooks are documented) the only supported surface.
- Transcript projector — the universal fallback: every such agent leaves a session transcript on disk. Tail or post-process it into
state/events.jsonland commit. Works for anything that writes a transcript, hooks or not — but transcript formats are usually undocumented internals; pin the agent version and expect breakage on upgrade. - Model/API proxy — run the agent behind a shim that sees its model calls and tool I/O; emit from there. Medium fidelity, for when there is neither hook nor transcript.
Whatever an agent exposes picks its rung. This is how any agent — named or not — slots in.
The lifting ceiling (inherent). Closed agents emit raw events (messages, tool calls), not semantic ones. The raw layer — tool_call, observation, model_call — maps nearly 1:1, cheap and faithful. The lineage spine — goal, hypothesis, patch, eval, decision — is not in a raw transcript; you infer it (a passing test command → eval; an accepted edit or commit → patch; the task prompt → goal), supply it out-of-band, or leave it thin. A closed agent therefore yields a transcript-faithful conformant log; the rich graph is only as good as what the adapter can lift. You cannot extract decisions the agent never externalized — a property of the source, not a defect of the adapter.
Export is easy; import is partial. You can rehydrate identity + skills + context into a fresh session of a closed agent — map identity onto its own config surface, prime context from the fold — but you cannot inject state back mid-tool-call, because you do not own its loop. Restore-into-a-closed-agent means "start a new session that knows who it is and what happened," not "resume exact loop state." Agents you own get full mid-loop resume (the restore contract); closed agents get session rehydration.
What this buys for closed agents: not resumability — a uniform, ownable record across vendors. Run one agent today and another tomorrow, point both adapters at the same repo, and identity, memory, and lineage are vendor-independent; they survive switching harnesses. One portable spine under a heterogeneous fleet.
The contract. An adapter is conformant if its emitted log (a) uses only the GASP vocabulary, (b) is appended durably and committed per the rules, and (c) round-trips — a conformant runtime can restore the resulting repo and continue. The unit of work is one adapter per agent (claude-code, codex, …), validated exactly as a native runtime is.
The cheap path for a Rust agent to satisfy Part I. A Python or TS agent implements the same layout itself and does not need this.
yoagent-state (v0.4.0, MIT) is an ActiveGraph-inspired continuity runtime. It records the append-only event log, folds it into the semantic graph, and ships the read-side primitives Part I only implies: lineage queries, replay, fork, diff, typed packs, policy gates, behavior subscriptions. You don't hand-author the log format — the runtime writes it via apply_ops.
Why this and not ActiveGraph directly. ActiveGraph is the inspiration, credited in the repo's ACKNOWLEDGMENTS.md. But it is Python, and the target agents are Rust single binaries shipped via brew/git-URL init; a Python sidecar or FFI in the hot path breaks the distribution story and fragments the event vocabulary across two implementations. Using ActiveGraph directly wouldn't even save work — it stores to its own event store, so a git adapter is needed either way. Being the canonical Rust implementation of "the log is the agent" is the stronger position; attribution plus protocol-compatibility claims the lineage without the dependency.
Persistence is the EventStore trait (append / scan / scan_after) — the seam between the graph logic (above) and storage (below); SnapshotStore, ForkStore, IndexStore, and ArtifactStore are sibling traits on the same seam. MemoryEventStore is the in-memory impl; git-backed durable state is another impl of the same trait, so the runtime stays git-unaware and intentionally small. The store is the place Part I's "append-only, committed log" rule is actually satisfied, and a naive append-and-commit gets two things wrong:
- Durable append, boundary commit — two different jobs. Appending a line to
events.jsonlandgit commitare separate operations. The JSONL append is the durability guarantee: flush each event immediately, so a crash mid-run loses nothing. The commit is the shipping/restore guarantee: one commit per run boundary, so history stays bisectable and you avoid commit spam. Committing per event destroys history; buffering in memory until the commit loses the tail on crash. The only correct shape is append-and-flush per event, commit at the run boundary. - The lease lives in the append path, not beside it. The single-writer lease and a git-backed append store protect the same thing — concurrent writes to one ref, the failure that corrupts SQLite-backed agents. If the store is the only writer and checks the lease inside its append, corruption is structurally impossible. Bolt the lease on beside the store and a second writer slips through.
Neither guarantee is part of GASP — another language's impl meets conformance rule 2 its own way.
Verified against the v0.4.0 source. load folds the log into the graph (the projector folds only state.ops_applied, exactly as Part I specifies). The paired typed helpers — record_goal, record_task, record_observation, record_failure, record_hypothesis, propose_patch, record_eval, record_decision_node, record_frame, plus record_run_started, record_run_finished, record_model_call, record_tool_call, record_project_snapshot, and the update_*_status family — emit a domain event and the state.ops_applied that mutates it, linked per the pairing rule. Events recorded during an open run auto-chain to run.started and carry the run id as correlation_id; the finish pair closes the folded run node (status: finished + outcome). (record_decision, record_eval_result, and link are lower-level ops-only paths; apply_ops is the raw escape hatch.)
let state = YoAgentState::load(store).await?; // fold events -> graph
state.record_goal(Goal::new(goal_id, "Make retry reliable", "...", actor)).await?;
state.record_eval(actor, eval_result, Some(patch_id)).await?; // eval + patch validated_by eval
state.record_decision_node(actor, decision, Some(patch_node)).await?; // decision + approved_by
print!("{}", state.lineage(node).await.to_markdown()); // the read side Part I impliesFor callback-driven agents, the YoAgentStateSink trait is the in-process adapter — on_run_started, on_run_finished, on_model_called, on_model_finished, on_tool_called, on_tool_finished — i.e. the integration contract as a typed interface; YoAgentStateAdapter is the shipped impl. Scoring uses the same primitives — patch/eval/decision, policy gates for the promotion gate (thresholds as config), fork for counterfactual A/B.
The store contract ships in 0.3.0+. Three EventStore impls: MemoryEventStore (tests), JsonlEventStore (small logs; rewrites the whole file per append, in-process lock only — kept for compatibility), and GitEventStore, which satisfies both store-contract guarantees: append + flush + fsync per batch (plus a parent-directory fsync on first creation), an atomically-acquired cross-process single-writer lease checked inside append, and pathspec-scoped boundary commits via commit_run(&RunId, &GoalId, outcome, extra_paths) carrying the Run-Id/Goal/Outcome trailers. The pairing rule is implemented (the paired helpers link each ops event's causation_id to its domain event), and run transitions are validated (double-start / mismatched-finish are errors). Repos emitted through the paired record_* helpers or the YoAgentStateSink adapter (conformant as of 0.4.0), on GitEventStore, pass all 7 conformance checks (Part VI).
Known 0.4.0 gaps: there is no snapshot emitter yet — the conformance checker is the executable definition of the snapshot format; the open-run marker is in-memory only (load does not recover an unfinished run, so a process restarted mid-run records uncorrelated roots until it starts a new run); the adapter's *_finished callbacks record raw events without updating the call nodes' outcome props; and the run structs' metadata fields are not persisted by the paired helpers.
Earlier drafts of this spec had the agent inventing two mechanisms the library already provides. Defer to the library:
| Earlier draft invented | yoagent-state primitive that replaces it |
|---|---|
| a flat session index file | goal/task/run events; fast query is a lineage query, not a grepped file |
| a separate scoring stream | the patch → eval → decision → promotion lifecycle |
Concrete adapters for two common coding agents — the outside-the-loop case of the adaptation contract. Transcript locations and hook surfaces shift between releases; treat every path and field below as a starting point and verify against current docs before building. (Terminology: yoagent-state's own projector module is the event→graph reducer and its adapter/YoAgentStateSink is the in-process event sink; the transcript adapters here are external and new — they emit the same events from an agent you don't control.)
- Rung 1 (hooks) — the primary path. Hooks are the documented, supported surface and the adapter should lead with them:
PreToolUse/PostToolUse(carryingtool_name,tool_input,tool_response),Stop,SessionStart/SessionEnd,UserPromptSubmit, subagent and compaction events, and more — every hook payload includessession_id,transcript_path, andcwd. Projection:PostToolUse→tool_call+observation,SessionStart/Stop→run.started/run.finished, withtranscript_pathlinking the cold archive. The Agent SDK can drive sessions programmatically and receives each message; sessions it creates persist to the same local store. - Rung 2 (transcript projector) — fallback, explicitly unstable. The session transcript is JSONL at
~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl, but the entry format is officially internal and changes between versions — the docs state that scripts parsing these files directly can break on any release. If you project from it anyway: top-level line types includeuser,assistant,system,summary(plus housekeeping types), linked byparentUuid, carryingcwd,gitBranch, and token usage; tool calls aretool_use/tool_resultcontent blocks insidemessage.content, not line types. Projection:tool_useblock →tool_call,tool_resultblock →observation, each assistant turn →model_call; theparentUuidchain gives causal ordering for free. Pin the Claude Code version, and treat the projector as per-version. - Lifting. A test/build command that passes →
eval; an applied edit or resulting git commit →patchreferencing that commit asartifact; the initiating user task →goal. Everything above the raw layer is heuristic. - Import. Map
identity/ontoCLAUDE.md, prime context from the fold; a new session starts knowing who it is and what happened.
- Rung 2 (transcript projector). Rollout transcript is
rollout-*.jsonl(recent versions compress to.jsonl.zst, first line aSessionMetaheader) under~/.codex/sessions/YYYY/MM/DD/, with session metadata indexed in a local SQLite (state_5.sqliteas of mid-2026; the rollout files remain the source of truth). There is no parent-event field — causality is inferred from order, so the projector reconstructs the chain positionally. Model/tool/observation projection is otherwise the same shape as Claude Code's. - Caveat. Codex's compaction summaries are genuinely ecosystem-locked: compaction is a server-side call returning an
encrypted_contentblob the client cannot read. Do not rely on them — project from the raw rollout lines, which you control. - Import. Map
identity/onto Codex'sAGENTS.mdsurface (its documented instruction chain); prime context from the fold.
The contract, not a per-agent special case. Whatever the agent exposes picks the rung: native callbacks → rung 1; a transcript on disk → rung 2; neither → run it behind a model/API proxy (rung 3). Write one adapter, emit the GASP vocabulary, validate by round-trip. An agent with no hooks, no transcript, and no proxyable boundary cannot be adapted — the only hard exclusion.
yoyo is the proof GASP works end to end. It is one agent; GASP is agent-agnostic. Its own motto is the layering:
yoagent executes. yoagent-state remembers. yoyo evolve improves. git ships.
Today yoyo-evolve depends on yoagent (the executor) but not yet on yoagent-state — the wiring below is the integration to build, not a description of what exists. The mapping (verified against the public repo, July 2026):
| yoyo today | GASP home | Notes |
|---|---|---|
IDENTITY.md / PERSONALITY.md (top-level, gate-protected) |
identity/ |
The 🐙 persona; evolve.sh's protected-file gate is the "immutable-by-policy" enforcement |
ECONOMICS.md (top-level) |
identity/PRINCIPLES.md |
Operating constraints |
scripts/yoyo_context.sh (identity loaded at runtime) |
restore step 3 | "Identity is runtime, never include_str!" = conformance rule 3 |
memory/learnings.jsonl → memory/active_learnings.md |
memory/facts.jsonl → active_memory.md |
Append-only-raw → regenerated-view; yoyo already honors the admission criterion — learnings, not history |
journals/ (+ dreams/) |
journal/ projection |
Now a projection of run events |
skills/ + skills_attic/ (top-level, auto-scanned) |
skills/ |
Already the right shape; the attic is retired versions |
| audit-log branch + session tags | transcripts/ cold store |
Session evidence already lives out of the hot path — a branch instead of a directory |
GH Actions: evolve.yml → evolve.sh: assess → plan → ≤3 tasks → cargo build+cargo test + evaluator gate → pass=commit, fail=git reset --hard + auto-filed issue |
the control loop + patch/eval/decision | "pass→commit, fail→revert" is the patch lifecycle |
| (no semantic log / lineage today) | state/events.jsonl via yoagent-state |
The core addition: lineage, replay, fork, version scoring |
Because yoyo's existing layout predates GASP, AGENT.md declares where identity and memory actually live (the manifest binds locations; the layout above is the default, not a straitjacket for adopters with history). The one thing yoyo gains is the semantic log itself — turning git history into a queryable lineage graph and making the self-improvement ratchet a primitive rather than an ad-hoc commit-or-revert script.
Skill evolution maps onto the Part I self-improvement pattern directly. yoyo's skill-evolve cycle (one refine | create | retire change per cycle, one commit per change) is the canonical case: each cycle is a run under a standing goal (goal_skill_quality), a completed change is a patch pinning the skill commit as its artifact, the harness gate (diff-scope hard rules + build/test) is the oracle producing the eval, and the keep/revert verdict is the decision. A retirement is a patch like any other — the commit it pins is the move into the attic; refused/NO-OP cycles close their run with that outcome and no patch. Two boundaries hold: (1) skill changes never enter memory/facts.jsonl — they are history, fully derivable from the patch spine, and the Memory admission criterion excludes them; a distilled insight behind a change may become a fact later, written by the agent's own synthesis with derived_from pointing at these events. (2) Mirror-on-change keeps conformance rule 3 honest during the two-repo interim: while the executor still carries its own live skill tree, every promoted skill change is mirrored into the state repo's skills/ (rebound to the state layout's paths) and rides the same boundary commit as its events — so a restored agent gets current skills, and each skill version ships with its lineage. The same session-end mirror keeps the memory streams current: new learnings convert to the fact envelope (derived_from naming the run that taught them) and append to facts.jsonl; journal entries append (the state copy honors check 4 even though the executor prepends); regenerable projections (active syntheses, dream arc, day counter) are overwritten. The end-state (the executor loading skills and memory from the state repo, retiring the mirrors) remains the migration target.
Inherent to the approach (any conformant impl inherits these):
- Compaction. The semantic log is far sparser than raw tokens, so you keep it whole. The lossy steps are the derived layers (
facts.jsonlextraction,JOURNAL.md,active_*.md) and the pruning of cold transcripts — a bad synthesis can distort the human-facing story. Keep the log + code commit always; regenerate projections freely; version the synthesis prompt; facts stay auditable viaderived_from. - World-state outside the log. Replaying events rebuilds the projection, not the filesystem or external APIs. Restore is faithful to what was logged, not the live world.
- Multiplayer merges. Append-only JSONL keyed by node id union-merges cleanly; semantic conflicts (contradictory observations/decisions) need application-level reconciliation, not
git merge. Keep identity merges human-gated. - Git scaling. Sparse semantic events keep file/commit counts far below per-token logging, but a long-lived agent can still approach GitHub's soft limits (≤3,000 dir entries; <1 GB ideal; files >100 MiB blocked). Shard
state/, snapshot on cadence, keep transcripts out of the main tree, use partial/shallow clone. Git is the right interchange substrate; at extreme scale the hot-path store may want a Merkle-chunked log. - Secrets/PII. Events are immutable and clonable, so a leaked secret is permanent and propagates on clone. Pre-commit secret scanning + redaction tokens; "right to be forgotten" fights append-only semantics.
- Closed-agent fidelity is bounded by liftability. Wrapping an agent you don't control (Part III) gives a transcript-faithful log, but the lineage spine is only as rich as the adapter can infer — you can't extract decisions the agent never externalized.
Reference-impl specific:
- Determinism. True transcript replay needs cached model/tool responses (an idea inherited from ActiveGraph). Not needed for the semantic-log hot path; only if you enable transcript replay.
- Maturity.
yoagent-stateis young (v0.4.0) and small by design — it deliberately does not handle identity/skills/memory, which is Part I's job, not a gap. The API will move (0.3.0 added run-transition validation, 0.4.0 changed adapter payload shapes); pin versions, track its CHANGELOG, and keep the event vocabulary protocol-compatible with ActiveGraph so the lineage claim stays clean. - Lease scope.
GitEventStore's file lease is decided by atomic exclusive create — sound on a local filesystem, advisory on network filesystems, and stealing an expired lease retains a narrow multi-process race window. One agent repo per machine (the intended deployment) is well inside the guarantee.
A spec is not a standard until "conformant" is verifiable. The kit is two things: a fixture every runtime must restore, and a checker every emitter must pass.
A minimal repo at the Part I layout, shipped as the reference:
fixture/
├── AGENT.md # spec_version, agent id, identity hash
├── identity/IDENTITY.md # a one-line constitution
├── skills/retry/SKILL.md # one skill, frontmatter + body
├── .agent/config.toml # executor binding (model, provider)
└── state/events.jsonl # the ten lines from the worked example above
Folding state/events.jsonl MUST yield a graph in which:
goal_retryexists and is the target of anadvancesedge frompatch_9;patch_9hasstatus = Promotedandreferences_commit = abc1234;patch_9 --validated_by--> eval_5, andeval_5.status = Passed;patch_9 --approved_by--> decision_3, anddecision_3.status = Approved.
A runtime that points at a fixture checkout, loads identity/ + skills/, folds the log, and reports that graph has proven Part I restore.
conformance-check <repo> runs seven checks and exits non-zero on any failure:
- Envelope round-trip. Every line parses to the event envelope (
id, schema_version, ts_ms, actor, kind, payload, causation_id, correlation_id) and re-serializes structurally equal — no unknown top-level fields, none missing. - Replay. Folding the log succeeds, and every snapshot equals the fold of the log prefix named by its integrity record (whose raw-byte hash and terminal event id must match the log — the Part I snapshot format). (If model/tool replay is enabled, responses must be cached so folds stay reproducible.)
- Vocabulary. Every
CreateNode.kindandCreateRelation.relis in the baseline vocabulary or in aPackthe repo declares at.agent/packs/*.json. Unknown kinds without a declaring pack fail. - Append-only-in-git. For every commit touching an append-only path (
state/events.jsonl,*/facts.jsonl,JOURNAL.md), the diff adds lines at EOF only — no modification or deletion of existing lines, and the working tree must extend the last committed version. Walk the git history (full history, no simplification); any in-place edit or deletion fails, a directory that is not a git repository fails (conformance rule 1), and any error during the walk fails closed. (This, not a pre-commit hook, is the enforcement.) - Causation integrity. Event ids are unique; every non-null
causation_idreferences an earlier event id in the log (which makes the chain acyclic); roots (null causation) are*.created/*.startedevents or ops-onlystate.ops_appliedmaintenance events (per the pairing rule). - Restore. The manifest (
AGENT.md) and identity are present and the log folds; run with--fixtureagainst the fixture, the fold must additionally reproduce the four asserted graph facts. (Manifest-declared alternate locations, skills loading, and identity-hash verification are the runtime's restore obligations — not yet mechanically checked here.) - Domain↔ops consistency (the pairing rule). Every entity-creating domain event (the Part I table:
goal.created,task.created,observation.created,failure.observed,hypothesis.created,patch.proposed,eval.finished,decision.created,frame.created) is materialized by exactly one pairedstate.ops_appliedwhoseCreateNodematches the payload's id, kind, and status — all claimants are checked, so a decoy pair cannot shadow a contradiction. An ops event chained to another ops event fails.
A runtime is conformant iff it can restore the fixture (check 6) and its own emitted repos pass checks 1–5 and 7. An adapter (Part III) is conformant iff its projected repo passes the same checks and round-trips through restore — the same bar, whether events came from a native loop or a wrapped agent's transcript. Conformance is per-emitter and mechanically checkable; that is what turns "compatible" from a claim into a test.