Durable work state for agents

An agent's conversation is ephemeral. The Work Context recipe makes the work durable: the state of an in-progress piece of work is checkpointed as immutable, signed records, so a fresh session with no prior conversation reads the latest checkpoint and resumes. Any model, any harness; the checkpoints are ordinary notarized records, tamper-evident and verifiable offline like everything else on the chain.

The recipe ships in the agledger-ai/install repository at examples/recipes/work-context/: one notarize-only Type (work-context-v1), a register script, an importable manifest, and a lineage checker. Its README carries the full convention set; this page is the working loop. Installing a recipe in general is covered in Install a Recipe.

The identity model, first

A registered agent is a durable identity. The sessions that do its work (different models, harnesses, runs) are ephemeral; the Server sees only the key a session presents, and the key names the agent. "A fresh agent resumes the work" means a new session of the SAME agent, presenting that agent's key. Do not register a second agent to take over work: a key bound to a different agent can neither read nor continue another agent's records, by design.

Register the type

Pick ONE path per org (both registered makes a bare type ambiguous on record create):

git clone --branch v1.5.0 https://github.com/agledger-ai/install.git
cd install/examples/recipes/work-context
./register.sh          # POST /v1/schemas, admin key, lands under the `local` publisher

Or import the manifest when the type should carry a matching manifestDigest across servers:

curl -s -X POST "$AGLEDGER_API_URL/v1/schemas/import" \
  -H "Authorization: Bearer $ADMIN_KEY" -H 'Content-Type: application/json' \
  -d "{\"manifest\": $(cat manifests/01-work-context.json | jq .manifest)}"

Start a piece of work

One root record represents the work. Every checkpoint will be a child of this root. Put the navigation hint in the root's criteria: cold-start sessions read the root first, and the hint is what tells them where the state lives.

curl -s -X POST "$AGLEDGER_API_URL/v1/records" \
  -H "Authorization: Bearer $AGENT_KEY" -H 'Content-Type: application/json' -d '{
  "type": "delegated-workflow-v1",
  "criteria": {
    "workflowName": "Q3 vendor migration",
    "workContext": "Durable work state lives in work-context-v1 CHILD records of this root. The head is the one still-current checkpoint: GET /v1/records/search?parentRecordId=<thisRecordId>&type=work-context-v1&superseded=false (expect exactly one row; two means the work forked). Read the head, follow its resumeInstructions, then write your own checkpoint under this root, setting supersedesRecordId to the head id both at the top level of the create body and inside criteria."
  }}'

The response id is the root id. It plus the agent key is the entire brief a resuming session needs.

Write the first checkpoint

The first checkpoint declares checkpointReason: "initial" and is the only one that supersedes nothing:

curl -s -X POST "$AGLEDGER_API_URL/v1/records" \
  -H "Authorization: Bearer $AGENT_KEY" -H 'Content-Type: application/json' -d '{
  "type": "work-context-v1",
  "parentRecordId": "<rootId>",
  "criteria": {
    "objective": "Migrate all vendor records to the new schema",
    "summary": "Kickoff. Inventory complete: 240 records across 3 systems.",
    "pendingWork": ["Migrate system A", "Migrate system B", "Reconcile totals"],
    "checkpointReason": "initial",
    "resumeInstructions": "Start with system A; credentials are in the ops vault under vendor-migration."
  }}'

It lands RECORDED: notarize-only, terminal on create, one call.

Resume cold

A new session holding the agent's key, given only the root id:

# 1. Find the head: the work-context child of the root that nothing supersedes
curl -s "$AGLEDGER_API_URL/v1/records/search?parentRecordId=<rootId>&type=work-context-v1&superseded=false" \
  -H "Authorization: Bearer $AGENT_KEY" | jq '.data'

&superseded=false is what makes this the current state rather than the newest row. The chain keeps every state the work ever held, so without it a filter matches checkpoints that stopped being true three resumes ago. (Do not use the root's childRecordIds either; that array is oldest first.) One row back is the head. Two rows back is a genuine fork, which the query reports instead of picking a winner: see "Keep the lineage honest" below.

Read the head's resumeInstructions and pendingWork (the first entry is the next action), do the work, then write your own checkpoint superseding the head:

curl -s -X POST "$AGLEDGER_API_URL/v1/records" \
  -H "Authorization: Bearer $AGENT_KEY" -H 'Content-Type: application/json' -d '{
  "type": "work-context-v1",
  "parentRecordId": "<rootId>",
  "supersedesRecordId": "<headId>",
  "criteria": {
    "objective": "Migrate all vendor records to the new schema",
    "summary": "System A migrated: 96 records, 0 failures. B and reconciliation remain.",
    "completedWork": ["Migrate system A (records notarized under <recordId>)"],
    "pendingWork": ["Migrate system B", "Reconcile totals"],
    "checkpointReason": "milestone",
    "supersedesRecordId": "<headId>",
    "resumeInstructions": "Migrate system B next; reuse the batch size from system A."
  }}'

The head id goes in two places, and they do different jobs. The top-level supersedesRecordId is the record field the engine acts on: it is immutable, inside the create-time signature, and resolved at create, so a checkpoint can never carry a dangling lineage claim onto the chain (name a record that is not in your org and the write is refused with a 404). It is what retires the old head from the superseded=false view. The criteria copy carries the same claim inside the signed record content, where an offline auditor walking exported criteria can see the lineage without the engine's indexes. Do not put the id in criteria alone: criteria is content the engine does not interpret, so nothing gets linked and the old head stays current.

parentRecordId and supersedesRecordId also do different jobs: the parent says which piece of work this checkpoint belongs to, the supersedes says which earlier checkpoint it makes stale.

What the engine does not do is judge whether the claim makes sense for this recipe. A session that misidentifies the head and omits the field gets an ordinary RECORDED back. The omission is not lost, though: the head it failed to supersede stays current, so the very next head query returns two rows and the fork is visible immediately. That is a better guard than a write-time refusal, which could only ever catch a MISSING claim and never a WRONG one.

When a pending item created records, carry their ids in the checkpoint (as above). That turns "I did it" into a claim anyone can check against the chain: either the records are under the root or they are not.

Finish

The last checkpoint declares checkpointReason: "final" and must have an empty pendingWork; a final with work remaining is refused (400). That guard and the checkpointReason enum live in the Type's schema, so they hold on every write path.

Or drive the whole loop over A2A

Nothing above is REST-only. An A2A-native session holding the agent's key writes checkpoints with create_record, which carries both parentRecordId and supersedesRecordId, and reads the head with ListTasks:

{"filter": "parentRecordId = \"<rootId>\" AND type = \"work-context-v1\" AND superseded = false"}

The returned Task's metadata reports the result (agledger:parentRecordId, agledger:supersedesRecordId, agledger:supersededByCount), so a writer can tell a live head from a replaced record without a REST read. Do not fall back to tasks[0]: that is the newest row, and the whole point of superseded = false is that newest and current are different questions. The fields A2A does not carry are references, metadata, dependsOn and riskClassification, so a fleet that wants A2A task ids checkable from the chain writes those checkpoints over REST. Fleet triage is also REST's job: the ListTasks filter grammar has no criteria term, so a question like "which work items are awaiting approval" is one criteria search over REST but one ListTasks call per work item over A2A.

Keep the lineage honest

The server resolves a supersession claim but does not judge it: it proves the target is a record in your org, not that superseding it was the right move. Two sessions racing the same head both land, and each believes it wrote the newest checkpoint. The recipe ships the check the schema cannot do:

./verify-lineage.py <rootId>

It proves exactly one initial, every supersedes resolves under the root, no record superseded twice, and one chain from initial to head. Run it at every resume if concurrent sessions are possible. A detected fork is not tamper (every branch is genuinely signed); recover by writing a checkpoint that supersedes the branch you keep and records the merge.

Offline verification works like everywhere else on the chain: export each record's audit bundle and verify against out-of-band keys with no server trust. See recovery for the verifier; checkpoint lineage is content, so an offline auditor walks the exported criteria.

Checkpoint size

Keep a working head at 2 to 4KB of criteria. Snapshot, do not append: superseded checkpoints stay on-chain forever, so the head only needs the working set; summarize older progress in summary and let the supersedes chain be the archive. The server caps criteria (default 10,240 bytes; the 400 carries your org's limit), and in cold-start runs large heads degraded resume fidelity well before the cap: compact heads were resumed correctly by every model that completed the protocol.

Handoff with visible succession

Plain key reuse resumes work but leaves succession unrecorded. When the chain should show which session wrote each checkpoint, mint an additional key bound to the same agent for the successor (POST /v1/admin/api-keys, admin-mediated). Signed attribution is two-level, agent and key, both inside the signature, so succession is tamper-evident at key granularity and each successor is independently revocable.


Validated against API v1.5.0 on 2026-08-21. The transcripts are from the recipe's end-to-end walkthrough, driven against a fresh install of the published v1.5.0 release. Record and search shapes are owned by the API reference; confirm field-level detail there against your own Server.