[Extension Proposal]: Expectations — letting the passage of time cause a state change
- Lenguaje dominante
- Shell
- Estrellas
- 25.7k
- Forks
- 2.6k
- Merge medio
- 3 d 6 h
- PR fusionados (30 d)
- 16
Descripción
### Summary
An opt-in extension that lets an agent state **what it expects to happen next, and by when** — and defines what the protocol does when it doesn't.
Every state change in A2A is caused by something arriving: a message, an answer, an event, a cancellation. Nothing lets the *passage of time* cause one. A task in `TASK_STATE_INPUT_REQUIRED` waits indefinitely. An agent that knows a document is due Thursday has no way to say so, and no way to be told on Friday that it never came. In the domains A2A is being deployed into, the failures that cost the most are absences, and an absence does not emit an event.
The extension adds four things, none of which change the meaning of a core request:
1. **Expectation** — raised in an Artifact's `metadata` (or an `input-required` status Message's), keyed by the extension URI: a named fact about a named subject, due by a stated UTC instant, satisfied by an exact `factKey` match. Immutable once raised; `dueAt` is required — there is no open-ended expectation.
2. **Satisfaction and re-anchoring** — a watcher records facts, satisfies expectations deterministically, and when an anchored step's predecessor lands, moves the dependent step's *current* due time while preserving its *projected* one. Original plan, current plan, actuals.
3. **Three kinds of lapse** — `overdue` (due + grace passed), `skipped` (a later step on the same subject was satisfied while this one was open — the report never arrived), `stalled` (the horizon passed with no fact of any kind on the subject). Each becomes a **new task** for the raising agent, under the same `contextId`, with `referenceTaskIds` pointing at the origin task and a `messageId` derived from `(expectationId, lapse, dueAt)` so at-least-once watchers produce one task.
4. **The watcher role** — the party that keeps time. The agent declares `keepsTime: false` on its card and never needs a clock; a client, orchestrator or service does the sweeping. This is the shape the maintainers described in #654 — a trigger creates a new task under one context — with the trigger specified.
Extension URI: `https://rsaxb.com/a2a/expectations/v1`
Specification: https://www.rsaxb.com/a2a/expectations/v1 (machine-readable descriptor and JSON Schema at `/extension.json`; the reasoning behind the non-obvious parts is written up at https://www.rsaxb.com/blog/your-agent-cannot-hear-silence)
**The agent expects. The watcher keeps time. The agent answers for the lapse.**
### Motivation
Without something in this shape, implementations resolve "the next thing never came" in one of four ways, all in production somewhere:
- **Wait.** `input-required` has no expiry, so an unanswered question is a task nobody closes.
- **Bolt a scheduler on.** A cron job outside the protocol reads the agent's store directly and pokes it. What the agent expected lives in the cron job's SQL, invisible to every client and to the agent.
- **Give the task a deadline.** A deadline bounds how long the *task* may run (#857). It says nothing about the world: an entry can be filed on time and the release still not come.
- **Hold the task `WORKING` for four days.** Waiting on a carrier, unreasonable to list, impossible to resume on a host that restarted.
Two things are deliberately kept out of the agent. The **clock**, because many agents cannot hold one (serverless, heartbeat-driven per #1667, or simply not running between requests). And the **decision that a lapse is unimportant**: an expectation the agent raised and quietly let go is indistinguishable, in the record, from one that was met. The lapse task exists so that the decision to do nothing is itself a task with a history.
### Design, in brief
An expectation, raised on a booking confirmation artifact:
```json
{
"expectationId": "exp-01J8Q0ARRIVE",
"kind": "shipment.arrival",
"subject": { "type": "shipment", "id": "PA-2026-0901" },
"expects": "an arrival event at DOH",
"satisfiedBy": { "factKey": "shipment.arrived:PA-2026-0901" },
"dueAt": "2026-09-11T05:00:00Z",
"grace": "PT6H",
"anchor": { "expectationId": "exp-01J8Q0DEPART", "offset": "PT1H45M" },
"after": ["exp-01J8Q0DEPART"],
"horizon": "2026-09-13T05:00:00Z",
"basis": "observed"
}
```
Four requirements are load-bearing, and are the parts we would most like argued with:
**`satisfiedBy` is a key, not a predicate.** The watcher is generic. A watcher that can evaluate "a manifest has probably been received" is a watcher with a model in it, and every property about determinism and attribution goes with it. The key is the whole contract between agent and watcher, and it is enough: the party that observes the manifest knows it is a manifest and knows the shipment.
**A lapse is a new task, never a mutation of the origin task.** Even a self-timing agent (`keepsTime: true`) MUST create the lapse as a task under the same context. Otherwise the record says "a task failed" rather than "an expectation the agent stated in advance went unmet", and the client holding the human finds out from a state change with no history behind it.
**`basis` is required and MUST be truthful** (`observed` | `contract` | `declared`). Due times taken from contracts or from what a lane is sold as fire on nearly every task; within a fortnight everyone ignores the colour and the one lapse that mattered is invisible. The first useful output of a deployment is usually the discovery that the published transit time and the real one have disagreed for a long while.
**Extending an expectation is bounded.** An agent that answers a lapse with "more time" MUST raise a new expectation carrying `supersedes`, MUST NOT raise one identical to an ancestor, and the watcher MUST bound lineage. After the bound, the only way an expectation gets more time is that a person gives it.
One rule that gets forgotten, stated normatively: when the origin task is canceled or fails, the watcher MUST **withdraw** every open expectation it raised. Lapse tasks for canceled work are what make an operations team turn the mechanism off.
### Relationship to existing issues
- **#857 (task deadlines)** — the nearest neighbour and solving a different problem. A deadline is the client's budget for the agent's work and belongs on the request. An expectation is the agent's statement about the world and belongs on the result. They compose: a task with a deadline may raise expectations that outlive it.
- **#210 (scheduled tasks, closed)** — cron is recurrence; this is a one-shot statement about a specific future fact. The DoS objection raised there applies in a narrower form and is bounded by the watcher (§13.2): an expectation cannot recur, and lineage is capped.
- **#654 (`input-required` clarification)** — the maintainer guidance there (new task per firing, one context) is adopted verbatim as the lapse-task shape.
- **#1667 (heartbeat agents)** — the reason the clock is outside the agent. `keepsTime: false` is the card-level statement that a watcher is required.
- **#2119 (cascading cancellation)** — withdrawal on the origin task's terminal state is the expectation-side counterpart.
- **#2144 (Learnables, ours)** — independent. Where both are deployed, *which* steps a lane carries is a learnable, and a lesson's `expiresAt` can raise a `lesson.review` expectation so its end is noticed rather than merely enforced.
### Out of scope
- **A scheduler or cron syntax.** The watcher's sweep is whatever the deployment already has.
- **How facts reach a watcher that is not the client**, and how the watcher stores its record.
- **Authorization.** A lapse task is authorized as any task from that watcher would be; the origin task's authority does not transfer. The spec requires this and does not define the mechanism.
### Status, and what we are asking for
This is a **draft**, published under a URI we control, which the governance document permits without registration. Not endorsed by or affiliated with the A2A project; no `a2aproject` prefix.
Implementation status, plainly: the design comes from a shipment-visibility system and an email agent in a private codebase, where "the event that never arrives" is the expensive case. The ledger (record / observe / sweep / withdraw, the three lapse kinds, anchoring, bounded `supersedes`) and the runtime wiring (card declaration with `keepsTime: false`, an expiry on `input-required` questions per §11, withdrawal on cancel/fail) exist in that codebase with tests written from the spec's Appendix B example; nothing sweeps it in production yet. There is no public reference implementation, so we do not meet the graduation bar and are not asking to.
What we are asking for is **discussion**. Specific critique most wanted on §9 (the three lapse kinds and re-anchoring — is `skipped` worth its complexity, or should it be a watcher-side heuristic?) and §10.3 / §13.2 (the loop bounds — is a lineage cap the right primitive?). If the field names are wrong, we would rather find out before anyone builds on them.
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.