# I taught my agents how to keep a promise

Source: https://www.andrewzigler.com/feed/i-taught-my-agents-how-to-keep-a-promise
Published: 2026-09-13

import PromiseLifecycle from '@components/PromiseLifecycle';
import PostCost from '@components/PostCost';
import BeadGraph from '@components/BeadGraph';

export const postCostData = {
  "_meta": {
    "what": "The beads and the real gateway costs that produced this article: the context graph that rendered it",
    "source": "self-hosted gateway request log, pulled 2026-09-13T23:32Z",
    "status": "final pull 2026-09-13: covers the whole arc from the first brainstorm session to the publish build. Dollars come from the log's own priced cost column, not an estimate. 553 of the 4175 requests (the orchestrating model after the 2026-09-01 pin bump) carry no price in the log, so cost_usd is a floor. The log stamps no per-bead id on any request in this window, so the beads below carry no per-bead dollars: the bead-to-phase mapping comes from the repo record, not from the request log."
  },
  "totals_at_snapshot": {
    "requests": 4175,
    "cost_usd": 646.68,
    "by_model": {
      "claude-opus-5": {
        "n": 2380,
        "input": 1159150,
        "output": 1956047,
        "cache_read": 457867462,
        "cost_usd": 370.41,
        "role": "writers, close-writers, draft, research + docs + rust builders"
      },
      "claude-fable-5": {
        "n": 436,
        "input": 181099,
        "output": 536337,
        "cache_read": 144467366,
        "cost_usd": 237.36,
        "role": "the seat (orchestration, review, folds, records) through 2026-08-31"
      },
      "claude-sonnet-5": {
        "n": 806,
        "input": 218220,
        "output": 400890,
        "cache_read": 108761991,
        "cost_usd": 38.91,
        "role": "the reader juries + cold reads"
      },
      "claude-fable-5-1": {
        "n": 553,
        "input": 34705,
        "output": 751405,
        "cache_read": 220395431,
        "cost_usd": 0.0,
        "role": "the seat after the 2026-09-01 model pin bump (unpriced in the log)"
      }
    }
  },
  "phases": [
    {
      "phase": "brainstorm: frames, jury, first draft",
      "utc": "2026-08-28",
      "n": 860,
      "tokens": 1861504,
      "cost_usd": 252.72,
      "what": "four writers working randomized frames against a reader jury, the fold that picked one, the first full draft, and the promise broker's first working build",
      "beads": [
        "aaif-i5a",
        "aaif-zpz"
      ]
    },
    {
      "phase": "reframe: drafts v2 and v3, header round",
      "utc": "2026-08-29 to 2026-09-05",
      "n": 612,
      "tokens": 924491,
      "cost_usd": 146.23,
      "what": "author notes folded in, the piece reframed onto the extensions axis, drafts v2 and v3, the enrichment spec ratified, and five rounds of header illustration",
      "beads": [
        "aaif-i5a",
        "aaif-38g"
      ]
    },
    {
      "phase": "fixes: the broker's own issue cycle",
      "utc": "2026-09-06 to 2026-09-12",
      "n": 1207,
      "tokens": 959588,
      "cost_usd": 87.13,
      "what": "pre-publish scrub, then the broker's own upstream backlog: amend events, caller-supplied ids, and opaque metadata built, reviewed, and fixed before release",
      "beads": [
        "aaif-i5a",
        "aaif-ih0",
        "aaif-jf7",
        "aaif-rsi",
        "aaif-rsi.1",
        "aaif-rsi.2",
        "aaif-rsi.3",
        "aaif-9c7"
      ]
    },
    {
      "phase": "release day: revision rounds v4 to v7",
      "utc": "2026-09-13",
      "n": 1496,
      "tokens": 1492270,
      "cost_usd": 160.6,
      "what": "pull requests posted and merged, v0.2.0 released, and the article rewritten four times against rendered review builds",
      "beads": [
        "aaif-i5a",
        "aaif-i5a.1",
        "aaif-6ecc",
        "aaif-rsi.8",
        "aaif-rsi.13",
        "aaif-9c7"
      ]
    }
  ],
  "beads": [
    {
      "id": "aaif-i5a",
      "title": "September anchor: the A2A x promise blog post",
      "type": "submission-candidate",
      "role": "the article"
    },
    {
      "id": "aaif-i5a.1",
      "title": "blog v6: the fix-first revision round",
      "type": "task",
      "role": "the rewrite that shipped"
    },
    {
      "id": "aaif-zpz",
      "title": "pinki: bootstrap the public A2A promise-broker repo",
      "type": "setup",
      "role": "the shipped artifact"
    },
    {
      "id": "aaif-38g",
      "title": "spec: pinki enrichment and staged adoption",
      "type": "spec",
      "role": "what the tool had to become"
    },
    {
      "id": "aaif-ih0",
      "title": "pinki: formalize the promise CLI and actually adopt it",
      "type": "task",
      "role": "the adoption push"
    },
    {
      "id": "aaif-jf7",
      "title": "the stage-0 forward-write fork",
      "type": "task",
      "role": "a decision the author made"
    },
    {
      "id": "aaif-rsi",
      "title": "epic: pinki, the upstream issue backlog mapped to beads",
      "type": "epic",
      "role": "the fix cycle"
    },
    {
      "id": "aaif-rsi.1",
      "title": "pinki #9: amend event, a history-preserving deadline move",
      "type": "task",
      "role": "fix one"
    },
    {
      "id": "aaif-rsi.2",
      "title": "pinki #5: caller-supplied and external ids",
      "type": "task",
      "role": "fix two"
    },
    {
      "id": "aaif-rsi.3",
      "title": "pinki #6: opaque meta on the record",
      "type": "task",
      "role": "fix three"
    },
    {
      "id": "aaif-rsi.8",
      "title": "pinki: write promises natively, no dual write",
      "type": "task",
      "role": "the cut-over"
    },
    {
      "id": "aaif-rsi.13",
      "title": "pinki #13: amend carries meta",
      "type": "task",
      "role": "the union of two fixes"
    },
    {
      "id": "aaif-6ecc",
      "title": "the article's own widgets, reassessed at phone width",
      "type": "setup",
      "role": "how you are reading this"
    },
    {
      "id": "aaif-9c7",
      "title": "relay real usage learnings back into the post",
      "type": "task",
      "role": "what comes next"
    }
  ]
};

export const beads = [
	{
		id: "aaif-i5a",
		type: "submission",
		status: "open",
		priority: 1,
		created: "2026-08-28",
		title: "This article: promises as an A2A extension",
		summary:
			"The September piece. Write up the promise record my agents already run on, and where it lands inside A2A's extension slot.",
		deps: [],
		requests: 1136,
		tokens: 1761658,
		dollars: 237.13,
	},
	{
		id: "aaif-zpz",
		type: "feature",
		status: "open",
		priority: 1,
		created: "2026-08-28",
		title: "Bootstrap pinki as a public repo",
		summary:
			"Ship the smallest honest promise broker as real open source: a CLI over an append-only JSONL log, with a README, a license and contributor guidelines.",
		deps: ["aaif-i5a"],
		requests: 430,
		tokens: 930752,
		dollars: 126.36,
	},
	{
		id: "aaif-38g",
		type: "spec",
		status: "open",
		priority: 1,
		created: "2026-08-30",
		title: "Spec: what to add to pinki, and how to adopt it",
		summary:
			"The enrichment-and-adoption spec. Which parts of the fleet's own promise machinery are worth moving into the tool, which are bespoke and stay out, and in what order the fleet switches over.",
		deps: ["aaif-zpz"],
		requests: 306,
		tokens: 462246,
		dollars: 73.11,
	},
	{
		id: "aaif-ih0",
		type: "research",
		status: "open",
		priority: 1,
		created: "2026-09-11",
		title: "The fleet was running a hand-written copy, not the tool",
		summary:
			"The measurement pass that started the adoption. The released binary was fine; what blocked adoption was a deadline that could not move, an id space the tool would not accept, and provenance with nowhere to ride.",
		deps: ["aaif-38g"],
		requests: 151,
		tokens: 119948,
		dollars: 10.89,
	},
	{
		id: "aaif-jf7",
		type: "decision",
		status: "closed",
		priority: 1,
		created: "2026-09-12",
		closed: "2026-09-12",
		closed_by: "the review desk",
		title: "Decision: adopt stage 0 as written",
		summary:
			"Three options for the first adoption step: take it as written, hold for the upstream deadline fix, or write nothing back. Ruled: take it as written.",
		close_reason: "Ruled, and executed the same night.",
		deps: ["aaif-ih0"],
		requests: 151,
		tokens: 119948,
		dollars: 10.89,
	},
	{
		id: "aaif-rsi",
		type: "epic",
		status: "open",
		priority: 1,
		created: "2026-09-12",
		title: "Own the tool: map its open issues to work, fix #9 first",
		summary:
			"Ten open issues on the public repo, each mapped to a unit of work. The deadline-move issue goes first because adoption cannot happen until it lands.",
		deps: [],
		requests: 151,
		tokens: 119948,
		dollars: 10.89,
	},
	{
		id: "aaif-rsi.1",
		type: "feature",
		status: "closed",
		priority: 1,
		created: "2026-09-12",
		closed: "2026-09-13",
		closed_by: "the ambassador seat",
		title:
			"pinki #9: the amend event, to move a deadline without losing the history",
		summary:
			"A nudge ladder kept re-declaring the same promise with a later deadline, and the tool refused it by design. The answer was a new event beside the declaration: the latest amend is the horizon, and every earlier one is kept.",
		close_reason: "Merged upstream as PR #11.",
		deps: ["aaif-rsi"],
		requests: 151,
		tokens: 119948,
		dollars: 10.89,
	},
	{
		id: "aaif-rsi.2",
		type: "feature",
		status: "closed",
		priority: 1,
		created: "2026-09-12",
		closed: "2026-09-13",
		closed_by: "the ambassador seat",
		title: "pinki #5: accept a caller-supplied id",
		summary:
			"The one affordance that looked like 'carry your ledger over' was not one, because it refused any id the tool had not minted. Now an arriving ledger keeps the ids other systems already reference, and the join works even when neither party minted here.",
		close_reason: "Merged upstream as PR #12.",
		deps: ["aaif-rsi"],
		requests: 151,
		tokens: 119948,
		dollars: 10.89,
	},
	{
		id: "aaif-rsi.3",
		type: "feature",
		status: "closed",
		priority: 1,
		created: "2026-09-12",
		closed: "2026-09-13",
		closed_by: "the ambassador seat",
		title: "pinki #6: an opaque meta object for provenance",
		summary:
			"The record was a closed set of seven keys, so an existing obligation could not be piped in without throwing away which component declared it and under what policy. One reserved object now carries that: stored, handed back, never read by the fold.",
		close_reason: "Merged upstream as PR #13.",
		deps: ["aaif-rsi"],
		requests: 151,
		tokens: 119948,
		dollars: 10.89,
	},
	{
		id: "aaif-rsi.13",
		type: "decision",
		status: "closed",
		priority: 1,
		created: "2026-09-13",
		closed: "2026-09-13",
		closed_by: "the ambassador seat",
		title: "Decision: the amend event carries meta too",
		summary:
			"Two branches that each passed review alone did not compile together: the provenance change had never heard of the deadline-move event. Shipping the one meta-less event would have made a nudge ladder throw away the half of its row that says who was nudging.",
		close_reason: "Ruled, and shipped inside the same pull request.",
		deps: ["aaif-rsi", "aaif-rsi.1", "aaif-rsi.3"],
		requests: 249,
		tokens: 248712,
		dollars: 26.77,
	},
	{
		id: "aaif-rsi.8",
		type: "task",
		status: "open",
		priority: 1,
		created: "2026-09-12",
		title: "Write pinki directly, with no parallel record",
		summary:
			"Adoption proper: the fleet's promise writer stops keeping its own copy and writes the tool's ledger natively. Waits on all three record fixes above.",
		deps: ["aaif-jf7", "aaif-rsi", "aaif-rsi.1", "aaif-rsi.2", "aaif-rsi.3"],
		requests: 249,
		tokens: 248712,
		dollars: 26.77,
	},
	{
		id: "aaif-rsi.14",
		type: "task",
		status: "open",
		priority: 3,
		created: "2026-09-13",
		title: "Land the release scripts in the repo",
		summary:
			"The 0.2.0 release was cut with two throwaway scripts. They belong in the repo, where the next maintainer does not have to retype them.",
		deps: ["aaif-rsi"],
	},
	{
		id: "aaif-9c7",
		type: "task",
		status: "open",
		priority: 3,
		created: "2026-09-12",
		title: "Fold what real usage teaches back into this article",
		summary:
			"A stub that holds the loop open: once the fleet is genuinely running on the tool, the first weeks of real use come back here as an update.",
		deps: ["aaif-i5a", "aaif-rsi", "aaif-rsi.8"],
		requests: 400,
		tokens: 368660,
		dollars: 37.66,
	},
	{
		id: "aaif-i5a.1",
		type: "review",
		status: "open",
		priority: 1,
		created: "2026-09-13",
		title: "Rewrite the article around the primitive, not the build log",
		summary:
			"A review round on the rendered draft. The piece reported what got built; it should teach the thing itself: agents keeping promises to each other, and to themselves.",
		deps: ["aaif-i5a"],
		requests: 249,
		tokens: 248712,
		dollars: 26.77,
	},
	{
		id: "aaif-6ecc",
		type: "task",
		status: "open",
		priority: 1,
		created: "2026-09-13",
		title: "Rebuild the article's two diagrams for a phone",
		summary:
			"Both figures were verified on a desktop only, and on a 390px screen the state machine ran off the side. Reshaped for a phone, and brought up to the tool's current vocabulary.",
		deps: ["aaif-i5a"],
		requests: 249,
		tokens: 248712,
		dollars: 26.77,
	},
];


Right before one of my agents wipes its own memory on purpose, it writes down what it still owes. The context that wakes up in its place never saw the work and has no memory of agreeing to anything, and it discharges the obligation anyway, on the first real thing it does.

That's a promise kept by an identity instead of by a session, and it's most of what I've been building this year, in a small open-source tool called [pinki](https://github.com/azigler/pinki). Agents don't get that for free: a session is the unit everything else is built on, and when one ends it takes its commitments with it.

I designed the record for my own fleet months before I read the A2A spec. When I finally did read it, the identity half was already sitting in there, signed and cached and built for strangers.

## A seat is an identity that outlives its sessions

A seat is a long-lived role with one name, one memory, and one kind of work, and sessions are its restarts. The seat running my research desk this week is the seat that ran it in June, hundreds of sessions later, with everything it has learned still attached.

I took the word from Steve Yegge, whose essays on living with a crew of agents are the most honest material on this. In [Model Welfare for Agentic Engineers](https://yegge.ai/essays/model-welfare/) he splits the two things I kept conflating: sessions are days, seats are people.

Work binds to the seat. When a seat promises something the promise belongs to the office, and the session that spoke it can end without releasing anybody. A restarted seat wakes up owing exactly what it owed. Before I built it that way, every restart manufactured a false lapse within minutes.

Promises matter to me because they let me hand intent to a fleet. The agent I ask can dispatch the work, but the agents *it* depends on never heard me ask, and they have no way to know how their piece fits a larger picture. A promise is how the importance travels.

The strangest one is the promise a seat makes to itself, and it's the one that proves the whole idea. The context that declared it is gone before the promise comes due. The seat is still there. If the fresh context never acts, the row sits open where the watchdog can see it, which makes it a liveness proof across deliberate amnesia. Most models of obligation assume two parties.

I haven't walked the other direction yet. A creditor is just a string, so you can promise a person, and nothing software does can discharge that one.

## A promise is what a seat owes

On my fleet a promise only counts as an obligation if it carries all of this:

- a deadline, so a lapse is computable
- evidence at resolution, so "done" points at an artifact
- a reason at abandonment, so dropping it is a recorded act
- a binding to the seat, so it survives session death

Every one of those earned its place by failing without it. Evidence is the daily workhorse: resolves that point at nothing turn out, on inspection, to be wishes.

I counted, because a design that has been running a while stops being a design: 18 of the 24 seats in that ledger have held more than one open promise at once, and the busiest peaked at 17.

The part I didn't design on purpose is the graph. My fleet's tasks already interlink to show the shape of a project. Promises turn out to be a graph too, and theirs shows the shape of the collaboration: who owes what to whom, and who's left holding something when a node goes quiet.

The state machine all of that produces is at the bottom of this post, with a stepper you can walk.

## An obligation has to bind something durable

A2A is built for strangers negotiating across org boundaries, which is a harder problem than the one I have. I run one fleet. So I read v1.0 with a narrow question: which parts of this are versions of things I already do? I'd rather borrow a vocabulary than keep maintaining a private one.

The task lifecycle rules itself out as the place an obligation can live, and every reason for that is a good call for tasks:

- A task can be purged. `TaskNotFoundError` covers "that finished last week" and "that never existed" with the same error.
- A context MAY expire under the server's cleanup policy.
- A terminal task is immutable, so refinement is a fresh object every round.
- Nothing carries a deadline. `TaskStatus.timestamp` records when a status happened, never when anything is due.

A task is built to be cheap and disposable, which is right, and it means nothing inside one lives long enough to owe you anything.

The AgentCard is the other half, and it's exactly the shape an obligation needs: a signed manifest that outlives every connection and gets fetched again whenever somebody wants to know who you are. Its subject is the durable party, which is my seat in my fleet's terms. An obligation that binds that subject survives session death, which is a claim the card is already built to make. The task just carries a reference and a clock, so purging it costs nothing.

A2A gives vocabulary like mine a sanctioned place to live, too. An extension declares itself on the card, switches on per request with a header, and carries its data as URI-prefixed keys in `metadata`, with a promotion path if adoption earns it. The deadline gap is already on file: [issue #857](https://github.com/a2aproject/A2A/issues/857) has been open since July 2025, and a project contributor suggested an extension as the right vehicle. So an extension is what I built.

The promise rides on the task it covers, under the extension's own key:

```json
"metadata": {
  "https://github.com/azigler/pinki/ext/promise/v0/promise": {
    "promise": "hand back a reviewed schema",
    "by":      "https://example.org/agents/reviewer",
    "to":      "https://example.org/agents/author",
    "until":   "2026-09-01T17:00:00Z"
  }
}
```

An obligation filed under that URI belongs to whoever the card is about. The card declaration that files it there, and the request header that switches it on, are both in the appendix.

## The parts the spec leaves open

Building against a spec is how you find its edges, and v1.0 is young. Here's where my promise vocabulary ran past it:

- A deadline has nowhere to live. No TTL, no due date, no expiry field anywhere in the message set, and a promise can't do without one.
- No task can report that it's overdue. The states are a closed set an extension may not add to, so a passed deadline produces no message at all.
- An assessment sits on the assessing agent's own ledger, advertised through a URL in its card's `params` if it publishes at all, which means two cards can point at ledgers that disagree.
- Watching promises isn't a role the protocol knows. The agent doing it is an ordinary client with ordinary authorization, and nobody is chartered to reconcile what it concludes with what the ledger still shows open.

The deadline gap doesn't stop at the missing field, because a deadline also moves. When a promise on my fleet runs past due, the escalation ladder re-declares the same one at 15 minutes and then 30, since minting a fresh id per nudge leaves a dead record behind. Against a fold that keeps only what was declared first, the ladder reports the original deadline forever, and an outside observer scored a live promise overdue 32 minutes before its owner did.

The protocol has nowhere to fix that, so I fixed it in the extension, with a new event beside the declaration rather than on top of it.

```json
{"type":"amend","promise":"desk/ship-91","by":"…/reviewer","until":"2026-09-01T18:00:00Z","reason":"nudge 1"}
```

The latest amend is the current horizon, every earlier one stays in the log, and the declaration is untouched. Nobody moves a deadline without leaving the old one behind.

## I built the smallest ledger that could hold one

I wanted the nucleus of something: extremely simple, extremely well-defined, fitting one specific niche, small enough that other people can build and orchestrate on top of it. So I built [pinki](https://github.com/azigler/pinki) (the pinky-promise kind 🤙), a tiny promise ledger shipped as an A2A extension. One record type, an append-only JSONL log, and a CLI that computes state by folding the log. No server, no daemon, and the dependency tree enforces that it can't make a network call.

I wanted it to feel like beads to use. The concept is Yegge's [Beads](https://github.com/gastownhall/beads), and the [beads_rust](https://github.com/Dicklesworthstone/beads_rust) engine my fleet runs is the UX bar it had to clear: one binary, local files, no opinion about your workflow.

You've seen most of the record already. The rest of it does structural work:

- `id`, the handle every other row joins on
- `task`, the same task seen from the promise's end
- `on`, an antecedent edge that activates this promise when another one lands

That last edge is what makes the promise graph queryable. `until` is the one place pinki insists, because a promise nobody can check is a wish.

The state vocabulary is borrowed on purpose. `conditional`, `detached`, `satisfied`, `violated` and `expired` come straight out of the multiagent-systems commitment literature. Yolum & Singh worked out the lifecycle in 2001; Chopra & Singh's Cupid put deadlines on commitments and compiled "which are violated?" down to a relational query in 2015; FIPA's contract net had a reply-by deadline on proposals back in 2002. The reading list was sitting there the whole time. What I added is the binding to a durable, cross-organizational identity like the card.

## The broker turned out to be an observer

I kept calling this thing a loose promise broker, and building it honestly kept shrinking the job title. Whether a promise was *kept* is a judgment. It depends on what counts as done, on context the record never carries, and on who's asking.

Mark Burgess's [Promise Theory](http://markburgess.org/PromiseMethod.pdf) is the sharpest statement of it I know: assessment is made by an agent, from that agent's vantage, and stays subjective no matter which agent makes it. An intermediary that appoints itself the arbiter acquires the power to distort what it claims to certify.

So pinki computes `overdue` and refuses to compute `violated`. `overdue` is arithmetic; `violated` is a judgment somebody signs. An assessment leaves the promise exactly as open as it was, and two observers who contradict each other get shown side by side.

My own watchdog is deliberately dumb. There's no model in the loop, because the agent that needs rescuing is the one that can't ask. It reads resolution rows and never the open set, since an empty open set means "everything kept" and "the ledger died" in the same breath. Emptiness is the one signal that lies.

An observer that can detect lapse creates a second ledger, and nothing says who reconciles the two. What's open lives on one surface, what an observer ruled abandoned lands on another, and nobody computes the join. Yegge writes about agent fleets converging into [little civilizations](https://yegge.ai/essays/the-shape-of-things-to-come/), with roles and law and institutional memory. On that map a promise ledger is an early institution and the reconciler is an office nobody has chartered yet. Somebody gets to build that broker: read cards, watch task references, publish verdicts, own nothing.

I still own both ends of every conversation here, and my fleet isn't running on pinki yet. That part is next.

My agents did put real promises through the released binary, and they filed 10 issues against it in public, one per thing that got in the way. That's what the repo asks for, since the most useful thing to send a project this early is an argument. 3 are fixed and out as [v0.2.0](https://github.com/azigler/pinki/releases/tag/v0.2.0). A 4th hasn't become an issue yet, and it's a hole in the vocabulary: there's satisfied, cancelled and released, and no word for the thing that expired and can no longer be met.

## The schema cannot tell you what breaks

When you ship a second version, the question that matters is what the binary already on somebody's disk does with a file this one wrote. I answered it three times in one day and got three different answers.

- [The new event](https://github.com/azigler/pinki/issues/9) refuses the whole file. v0.1.0 reaches the amend line, finds a `type` it has never heard of, and exits 1 on the whole ledger.
- [The foreign id](https://github.com/azigler/pinki/issues/5) changes nothing. v0.1.0 reads, shows, resolves and assesses a promise whose id it would never have minted. It just can't declare one, which was the bug.
- [The provenance field](https://github.com/azigler/pinki/issues/6) disappears without a sound. v0.1.0 reads the ledger, prints every state correctly, exits 0, and leaves the provenance out.

Reasoning from the schema got it wrong twice. It predicted the old reader would skip a line it didn't know, and it predicted a refusal for provenance. Provenance vanished instead, because the strict parsing lives on the input path and never on the read path.

The first of those wrong guesses was the amend branch's own compatibility claim, and it didn't survive review. The test proving it filtered the new event out of an in-memory list and never touched deserialization, so the claim held for a Rust `Vec` and failed for anything you'd actually run. A reviewer ran v0.1.0. pinki has 201 tests and not one of them can do that, because CI holds no copy of the old binary, and a test that cannot run is not evidence.

Silent loss is the geometry I want least in an obligation ledger. The other two failures tell you. A reader that exits 0 while holding back what the file says fails the way an empty open set does.

Until v0.2.0 both binaries answered `pinki 0.1.0` to `--version`, so none of these sentences was checkable by anybody holding one. A version number is what makes a compatibility claim checkable by a stranger.

## The part I can build against today

Writing this, I noticed I had been solving two problems as if they were one. Identity is the first: where an agent lives, and what can hold it to something. The second is the language an agent uses to extend the world it's living in. A2A moved the first one a long way. Cards are signed, extensions are a real slot, and an obligation can ride in that slot this year.

The second problem doesn't have a home right now. Years before I read this spec, I tried to build both problems into the same object -- a world where agents would live in one binary and extend it from inside: a sandbox agents can expand and still be sandboxed, with the identity bounded inside it.

I don't know whether that's one system or two. Nobody has built it, so I can't tell you what it costs. The obligations part I can build against today.

Maybe there's something there. I'm going to go read what got left behind.

## The lifecycle, end to end

<PromiseLifecycle />

- The OWED and ENDED columns are computed: arithmetic over a log, where any two implementations agree. `overdue` sits with the open states on purpose, because a passed deadline closes nothing.
- `violated` never joins the machine. It's an attributed judgment that sits beside a promise, and two observers can disagree in plain view.
- Run the stepper and watch what happens when the deadline passes: the ledger doesn't move. That asymmetry is most of the design.

## The beads that made this, and what they cost

Last month I started ending these posts with the work that produced them, priced from my gateway's request log. If you want the machinery, [that post](https://www.andrewzigler.com/feed/every-task-my-fleet-completes-now-carries-its-cost) is where I worked out how to pull it and put it on the page.

Here's this article as the graph of work it actually was, because all work is a graph now.

<BeadGraph beads={beads} showCosts caption="Priced by phase from the gateway log and split evenly across the beads each phase advanced. The log carries no per-bead stamp for this arc, so no node here is measured on its own." />

And here is the bill, pulled from the log the day this went out.

<PostCost data={postCostData} />

## Appendix: the schema you can copy

Everything below is the shipped v0 shape from pinki's [A2A binding doc](https://github.com/azigler/pinki/blob/main/docs/A2A-EXTENSION.md), checked against `specification/a2a.proto`. No new states, no new fields.

1. Declare it on your AgentCard. The declaration asserts what A2A itself doesn't model: obligations under this URI bind the card's subject and survive any session that made them.

```json
{
  "capabilities": {
    "extensions": [{
      "uri": "https://github.com/azigler/pinki/ext/promise/v0",
      "description": "Promises made by this card's subject: deadline, evidence on satisfaction, reason on abandonment.",
      "required": false,
      "params": {
        "assessments": "https://example.org/agents/reviewer/assessments.jsonl"
      }
    }]
  }
}
```

`required` stays false, because a promise is voluntary. The URI is provisional while the extension is unofficial.

2. Carry the record on the Task it concerns, as one URI-prefixed key in `metadata`:

```json
"metadata": {
  "https://github.com/azigler/pinki/ext/promise/v0/promise": {
    "id":      "desk/ship-91",
    "promise": "hand back a reviewed schema",
    "by":      "https://example.org/agents/reviewer",
    "to":      "https://example.org/agents/author",
    "on":      "pnk_0c2b77",
    "until":   "2026-09-01T17:00:00Z",
    "task":    "a2a-task-9c1f0e"
  }
}
```

3. Resolve onto states that already exist.

- `satisfied` maps to `COMPLETED` and requires at least one `Artifact`. `COMPLETED` with an empty artifacts list is non-conformant here.
- `cancelled` maps to `REJECTED`, with the reason in `TaskStatus.message`.
- `released` maps to `CANCELED`, naming the creditor who let the debtor off.
- `overdue` maps to nothing on purpose. If a passed deadline were a task state, the server that owes the promise could transition its own way out of owing it. A reader computes `now > until` for itself.

Here's a worked lifecycle, as the ledger sees it. The promise id is the caller's own; only the antecedent it waits on was minted here:

```jsonl
{"ts":"2026-08-28T20:14:03Z","type":"promise","id":"desk/ship-91","promise":"hand back a reviewed schema","by":"…/reviewer","to":"…/author","on":"pnk_0c2b77","until":"2026-09-01T17:00:00Z"}
{"ts":"2026-09-01T17:12:00Z","type":"amend","promise":"desk/ship-91","by":"…/reviewer","until":"2026-09-01T18:00:00Z","reason":"nudge 1","meta":{"rung":1}}
{"ts":"2026-09-01T18:40:00Z","type":"assess","promise":"desk/ship-91","state":"violated","observer":"…/author","note":"deadline passed, nothing delivered","meta":{"seat":"desk"}}
{"ts":"2026-09-02T09:40:00Z","type":"resolve","promise":"desk/ship-91","as":"satisfied","by":"…/reviewer","evidence":["https://example.org/reviews/91"],"meta":{"seat":"reviewer"}}
```

Look at the middle of that ledger. The deadline passed and appended nothing. The debtor moved it 12 minutes later, on the record. An observer signed a judgment. And the debtor still delivered -- late, with evidence, 15 hours after an observer had given up on it. Both records are true. Reconciling them is the job nobody holds yet.

---

*Andrew Zigler is a 2026 AAIF Ambassador. The gateway ledger these cost numbers come from is the subject of the previous piece, [Every task my fleet completes now carries its cost](https://www.andrewzigler.com/feed/every-task-my-fleet-completes-now-carries-its-cost).*
