GASP: Your Agent Is a Git Repo

GASP — the Git Agent State Protocol — keeps an agent's durable self in a git repository: an append-only semantic event log (state/events.jsonl) that folds into a typed goal/run/model/tool graph, alongside identity, skills, and memory tiers. Restore = git clone + replay. Clone your agent onto a new machine and it remembers everything, with lineage.

yoagent bridges to GASP through the gasp feature (backed by yoagent-state, the reference runtime). The bridge is a consumer of the AgentEvent stream — zero agent-loop changes:

yoagent = { version = "0.24", features = ["gasp"] }

The gasp feature needs the filesystem and git, so it is native-only: it is not available on wasm32 (see WebAssembly & Cloudflare Workers).

#![allow(unused)]
fn main() {
use yoagent::gasp::{GaspRecorder, GoalRef};

let recorder = GaspRecorder::init(
    "./my-agent-repo", "my-agent", "worker-1",
    GoalRef::New { title: "ship the feature".into() },
).await?;

let (tx, record_handle) = recorder.recording_sender("implement the parser", None);
agent.prompt_with_sender("implement the parser", tx).await;
let run_id = record_handle.await??;   // Option<RunId>: None if no AgentStart arrived
}

What gets recorded

Agent activityGASP events
loop startsrun.started (the goal is stamped in the run commit's Goal: trailer)
each assistant turnmodel.called / model.finished paired nodes (a retried provider attempt — an error MessageEnd followed by ProviderRetry — is not recorded)
each tool executiontool.called / tool.finished (with success flag)
a plugin logs (opt-in, below)observation.created, linked to the run, {source: "plugin", level} metadata
loop endsrun.finished with the outcome: completed / truncated / error / aborted (a cancelled run) / refused, loop_aborted:<tool> when loop detection stopped it, rejected when an input filter refused the prompt, interrupted for a run a crashed process left open

The semantic log stores bounded one-line summaries — the task string (verbatim), model ids, and the first 200 characters of tool inputs, tool outputs, and assistant text — never full transcripts. If secrets can flow through tool arguments or outputs, install a redacting summarizer (recorder.with_summarizer(...)) before recording: the repo is designed to be cloned and shared, and committed history is hard to scrub. Full transcripts belong in GASP's cold transcripts/ tier — Session::to_jsonl is a natural format for it.

Robustness: a crashed process leaves no open run — the recorder closes stale runs as interrupted on open, and a dropped event stream finishes the run with the outcome derived so far. One git commit per run keeps the history append-only, and the init scaffolding (manifest, identity) is committed so a fresh clone is a complete, conformant agent. If recording fails mid-run (disk, lease, git), recording stops and the error surfaces via the returned handle — always await it — while event forwarding to your UI continues uninterrupted. GASP repos are single-writer: don't share one repo between live workers, and record one run at a time.

Plugin logs (opt-in)

A rutis plugin's diagnostics — the yoagent-rutis bridge's log, such as a pi extension's "command /todos not available" or a DSH image that could not be read — can be part of the run's record. Install two things from the recorder:

use tracing_subscriber::layer::SubscriberExt;

tracing::subscriber::set_global_default(
    tracing_subscriber::registry().with(recorder.plugin_log_layer()),
)?;
let agent = agent.with_extension(recorder.extension()); // learns each run's id

tracing-subscriber 0.3 must be a dependency of your own crate (the layer implements its Layer trait). The layer forwards lines of the target yoagent_rutis::plugin (gasp::PLUGIN_LOG_TARGET) whose run_id is one of this recorder's runs — another agent's plugins in the same process never land here, and a line without a run stays in your logs only. Each line goes through the summarizer, like tool output, and is recorded as an observation.created on the run. Installed with with_tree_extension, delegated runs' lines are recorded on the same run. Record every run of an agent that has the extension: a run started without a recording while one is open is attributed to that recording.

Tested conformance

yoagent's CI emits an agent repo with a mock provider and runs the GASP conformance checker against it — all seven mechanical checks (envelope round-trip, replay, vocabulary, append-only git history, causation integrity, restore, domain↔ops consistency) must pass on every commit. Try it yourself:

cargo run --example gasp_emit --features gasp -- /tmp/my-agent
git clone https://github.com/yologdev/gasp && cd gasp/conformance-check
cargo run -q -- /tmp/my-agent
# conformant: all checks passed