@openwop/spec-artifacts 2.42.5 → 2.42.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CORPUS-STAMP.json +64 -63
  2. package/api/seams-v2.yaml +238 -316
  3. package/api/v2/asyncapi.yaml +83 -113
  4. package/api/v2/openapi.yaml +339 -724
  5. package/package.json +1 -1
  6. package/schemas/audit-verify-result.schema.json +2 -2
  7. package/schemas/v2/audit-verify-result.schema.json +2 -2
  8. package/schemas/v2/capabilities.schema.json +2 -5
  9. package/spec/v1/core-standard-manifest.json +2 -2
  10. package/spec/v1/gaps.json +171 -1
  11. package/spec/v2/README.md +34 -36
  12. package/spec/v2/core/capabilities.md +186 -119
  13. package/spec/v2/core/conformance.md +78 -27
  14. package/spec/v2/core/connection-packs.md +8 -3
  15. package/spec/v2/core/conversation.md +10 -4
  16. package/spec/v2/core/errors.md +30 -9
  17. package/spec/v2/core/events.md +116 -80
  18. package/spec/v2/core/form-content-packs.md +11 -13
  19. package/spec/v2/core/headers.md +22 -14
  20. package/spec/v2/core/host-services.md +22 -5
  21. package/spec/v2/core/idempotency.md +48 -36
  22. package/spec/v2/core/identity.md +110 -42
  23. package/spec/v2/core/interop.md +82 -21
  24. package/spec/v2/core/interrupt.md +79 -40
  25. package/spec/v2/core/node-pack-runtimes.md +18 -4
  26. package/spec/v2/core/oauth.md +40 -6
  27. package/spec/v2/core/overview.md +61 -33
  28. package/spec/v2/core/packs.md +53 -28
  29. package/spec/v2/core/persistence.md +61 -80
  30. package/spec/v2/core/replay.md +56 -42
  31. package/spec/v2/core/runs.md +198 -60
  32. package/spec/v2/core/security-defaults.md +57 -33
  33. package/spec/v2/core/tool-catalog.md +29 -6
  34. package/spec/v2/core/versioning.md +95 -49
  35. package/spec/v2/core/webhooks.md +59 -20
  36. package/spec/v2/core/workflow-chain-packs.md +16 -14
  37. package/spec/v2/corrections.json +17 -1
  38. package/spec/v2/corrections.schema.json +1 -1
  39. package/spec/v2/declaration.json +24 -7
  40. package/spec/v2/declaration.schema.json +10 -0
  41. package/spec/v2/errors.json +109 -2
  42. package/spec/v2/errors.schema.json +38 -0
  43. package/spec/v2/ext/README.md +36 -23
  44. package/spec/v2/ext/a2uiSurface/README.md +75 -89
  45. package/spec/v2/ext/brand/README.md +15 -15
  46. package/spec/v2/ext/canvas/README.md +15 -15
  47. package/spec/v2/ext/chat/README.md +15 -15
  48. package/spec/v2/ext/coordination/README.md +15 -15
  49. package/spec/v2/ext/dataIntegration/README.md +17 -20
  50. package/spec/v2/ext/entities/README.md +15 -15
  51. package/spec/v2/ext/grpc-transport/README.md +6 -8
  52. package/spec/v2/ext/kanban/README.md +15 -15
  53. package/spec/v2/ext/knowledge/README.md +15 -15
  54. package/spec/v2/ext/launchStudio/README.md +16 -16
  55. package/spec/v2/ext/messaging/README.md +15 -15
  56. package/spec/v2/ext/portability/README.md +3 -17
  57. package/spec/v2/ext/provider-idempotency/README.md +7 -10
  58. package/spec/v2/ext/restTransport/README.md +30 -19
  59. package/spec/v2/ext/sandbox-runtime-notes/README.md +7 -9
  60. package/spec/v2/ext/webResearch/README.md +16 -16
  61. package/spec/v2/profiles.json +6 -2
  62. package/spec/v2/release.json +2 -2
  63. package/spec/v2/surface-baseline.json +542 -2
@@ -1,6 +1,6 @@
1
1
  # Replay and Fork
2
2
 
3
- > **Status: Stable · RFC 0140, 0041, 0173 §C, 0176 §A.5.**
3
+ > **Status: Stable.**
4
4
  > **Normative home:** `eventLog`, `replay`, `nondeterminismPolicy`.
5
5
 
6
6
  ## Why this exists
@@ -9,14 +9,23 @@
9
9
 
10
10
  ## The surface
11
11
 
12
- A host that advertises `replay` (capabilities.md) serves `forkRun` (`api/v2/openapi.yaml`, `POST /runs/{runId}:fork`) and `getEffectSeamManifest` (`GET /host/effect-seams`). The `replay` facet (`spec/v2/facets/replay.schema.json`) is `{ modes[], retention?, effectSeamsManifest }`; `modes` enumerates `replay | branch` (the `forkRun` `mode` values); `effectSeamsManifest` is the constant `/host/effect-seams`. There is no `sideEffectSuppression` field: suppression is the only conforming replay behavior (RFC 0173 §B, row `C6.2`) and `none` is not a value.
12
+ A host that advertises `replay` ([capabilities.md](capabilities.md)) serves `forkRun` (`api/v2/openapi.yaml`, `POST /runs/{runId}:fork`) and `getEffectSeamManifest` (`GET /host/effect-seams`).
13
13
 
14
- The request body, `fromSeq` defaults and `201` response are runs.md §Fork. Events with `sequence < fromSeq` are fixed history; events `>= fromSeq` are re-executed.
14
+ The `replay` facet (`spec/v2/facets/replay.schema.json`) is `{ modes[], retention?, effectSeamsManifest }`:
15
15
 
16
- | Rule | Requirement |
17
- | --- | --- |
18
- | `fromSeq` out of range | `400`; a sequence absent from the source log is `422`. A `fromSeq` greater than the sequence of the source run's terminal run event MUST be refused `422 fork_point_invalid` — the fork would inherit a terminal event and then execute (RFC 0194 §C; binds only where a compensation tail follows the terminal event). |
19
- | Source run not visible to the caller | `404`. |
16
+ - `modes` enumerates `replay | branch` (the `forkRun` `mode` values);
17
+ - `effectSeamsManifest` is the constant `/host/effect-seams`.
18
+
19
+ Suppression is the only conforming replay behavior; no field turns it off, and `none` is not a value.
20
+
21
+ The request body, `fromSeq` defaults and `201` response are [runs.md §Fork](runs.md). Events with `sequence < fromSeq` are fixed history; events `>= fromSeq` are re-executed.
22
+
23
+ Refusals:
24
+
25
+ - `fromSeq` out of range — `400`.
26
+ - A sequence absent from the source log — `422`.
27
+ - A `fromSeq` greater than the sequence of the source run's terminal run event MUST be refused `422 fork_point_invalid`: the fork would inherit a terminal event and then execute. This binds only where a compensation tail follows the terminal event.
28
+ - Source run not visible to the caller — `404`.
20
29
 
21
30
  ## Modes
22
31
 
@@ -26,13 +35,13 @@ The request body, `fromSeq` defaults and `201` response are runs.md §Fork. Even
26
35
 
27
36
  ## Byte-equivalence of the prefix
28
37
 
29
- The replay contract is observable-output-sequence determinism, not bit-equivalent execution (RFC 0041 §C):
38
+ The replay contract is observable-output-sequence determinism, not bit-equivalent execution:
30
39
 
31
- 1. The events at indices `[0, fromSeq)` MUST be byte-equivalent between source and replay, modulo per-region clock fields (RFC 0036 §E) and ULID time-component entropy when ULIDs are minted fresh. The event at `fromSeq` is governed by §Divergence.
40
+ 1. The events at indices `[0, fromSeq)` MUST be byte-equivalent between source and replay, modulo per-region clock fields and ULID time-component entropy when ULIDs are minted fresh. The event at `fromSeq` is governed by §Divergence.
32
41
  2. `variables`, `channels`, and `status` of the run snapshot at each index in that range MUST be byte-equivalent.
33
42
  3. The bytes on the wire of underlying tool and LLM calls MAY differ, provided the observable state at each index is byte-equivalent.
34
43
 
35
- A host MUST cache the observable result (return value, workflow-state effects, emitted events), not merely the tool-call boundary. The cache key for LLM-calling nodes is the RFC 0041 content-addressed key; for other tool-calling nodes it MUST be content-addressable, never a host-internal sequence number or timestamp.
44
+ A host MUST cache the observable result (return value, workflow-state effects, emitted events), not merely the tool-call boundary. The cache key for LLM-calling nodes is the content-addressed invocation key defined in [RFC 0041](https://github.com/openwop/openwop/blob/main/RFCS/0041-multi-agent-replay-under-nondeterminism.md); for other tool-calling nodes it MUST be content-addressable, never a host-internal sequence number or timestamp.
36
45
 
37
46
  ## Determinism caveats (`replay` mode)
38
47
 
@@ -40,23 +49,27 @@ A host MUST cache the observable result (return value, workflow-state effects, e
40
49
  2. `ctx.interrupt(K)` MUST short-circuit to the persisted `interrupt.resolved` value.
41
50
  3. `ctx.getVersion` pins from the source run are fixed history; the replay MUST take the recorded branch.
42
51
  4. Nodes MUST consume time via `ctx.now()` where available; direct clock reads make replay non-deterministic.
43
- 5. Recorded-fact events such as `memory.written` (RFC 0057) are fixed history. A replay MUST re-emit them verbatim from the log and MUST NOT regenerate their identifiers or timestamps — never a new `memoryId`. A `branch` MAY perform its own memory writes with fresh identifiers.
44
- 6. Approver eligibility recorded on a resume event is fixed history; a host MUST NOT re-resolve membership during replay (RFC 0104).
52
+ 5. Recorded-fact events such as `memory.written` are fixed history. A replay MUST re-emit them verbatim from the log and MUST NOT regenerate their identifiers or timestamps — never a new `memoryId`. A `branch` MAY perform its own memory writes with fresh identifiers.
53
+ 6. Approver eligibility recorded on a resume event is fixed history; a host MUST NOT re-resolve membership during replay.
45
54
 
46
55
  ## Divergence
47
56
 
48
- When a replayed node produces an event different from the source at the same sequence, the host MUST continue, MUST emit `replay.diverged` `{ originalEventId, replayEventId, divergencePoint }`, and MUST surface it in `debug` stream mode and as OTel attribute `openwop.replay.diverged: true`.
57
+ When a replayed node produces an event different from the source at the same sequence, the host:
49
58
 
50
- | Code (`spec/v2/errors.json`) | Where | Condition |
51
- | --- | --- | --- |
52
- | `replay_diverged_at_refusal` | fork fails, `409` | The source obtained a valid envelope and the replay a refusal, or the reverse. The host MUST NOT substitute silently; it MUST emit `replay.diverged-at-refusal` naming the node and both envelope kinds and fail the replay with this code (RFC 0041 §B). |
53
- | `replay_source_missing` | `node.failed` payload; the fork request still returns `201` | A side-effecting node reached with no recorded source outcome for `(nodeId, attempt)` (§Suppression). |
54
- | `replay_memory_snapshot_unavailable` | fork refused, `409` | The host cannot serve memory state as-of `fromSeq`. It MUST refuse rather than substitute current memory; `details.fromSeq` SHOULD name the index (RFC 0039 §B). |
55
- | `replay_context_summary_unavailable` | fork refused, `409` | The host advertises `multiAgent.executionModel.contextBudget.summarization` and cannot serve, as-of `fromSeq`, a summary artifact (`context.summarized.summaryRef`) the replay would reuse. It MUST refuse rather than re-summarize; `details.fromSeq` SHOULD name the index (RFC 0111). |
59
+ - MUST continue;
60
+ - MUST emit `replay.diverged` `{ originalEventId, replayEventId, divergencePoint }`;
61
+ - MUST surface it in `debug` stream mode and as OTel attribute `openwop.replay.diverged: true`.
62
+
63
+ Divergence codes (`spec/v2/errors.json`):
64
+
65
+ - **`replay_diverged_at_refusal`** (fork fails, `409`) — the source obtained a valid envelope and the replay a refusal, or the reverse. The host MUST NOT substitute silently; it MUST emit `replay.diverged-at-refusal` naming the node and both envelope kinds and fail the replay with this code.
66
+ - **`replay_source_missing`** (`node.failed` payload; the fork request still returns `201`) — a side-effecting node reached with no recorded source outcome for `(nodeId, attempt)` (§Suppression).
67
+ - **`replay_memory_snapshot_unavailable`** (fork refused, `409`) — the host cannot serve memory state as-of `fromSeq`. It MUST refuse rather than substitute current memory; `details.fromSeq` SHOULD name the index.
68
+ - **`replay_context_summary_unavailable`** (fork refused, `409`) — the host advertises `multiAgent.executionModel.contextBudget.summarization` and cannot serve, as-of `fromSeq`, a summary artifact (`context.summarized.summaryRef`) the replay would reuse. It MUST refuse rather than re-summarize; `details.fromSeq` SHOULD name the index.
56
69
 
57
70
  ## Suppression
58
71
 
59
- Suppression is an obligation of the `replay` surface (RFC 0173 §A.1, §B): advertising `replay` binds it, and a host that cannot suppress MUST NOT advertise `replay`. A relaxation is an operator setting recorded in the certification bundle (security-defaults.md).
72
+ Suppression is an obligation of the `replay` surface: advertising `replay` binds it, and a host that cannot suppress MUST NOT advertise `replay`. A relaxation is an operator setting recorded in the certification bundle (security-defaults.md).
60
73
 
61
74
  For a fork with `mode: replay`:
62
75
 
@@ -69,53 +82,54 @@ For a fork with `mode: replay`:
69
82
 
70
83
  Pure nodes and LLM calls served from the invocation log MUST re-execute live; otherwise divergence detection is vacuous.
71
84
 
72
- **Fan-out.** A host that projects its log outward — webhook delivery, A2A push, outbound streams, analytics or audit sinks — MUST NOT deliver events a replay re-emits as fixed history, and a fork of either mode MUST NOT inherit its source's A2A push configs. Replay-ness MUST be read from the run, never from the event type; the fork's own log MUST still carry the re-emitted events (webhooks.md).
85
+ **Fan-out.** A host that projects its log outward — webhook delivery, A2A push, outbound streams, analytics or audit sinks — MUST NOT deliver events a replay re-emits as fixed history, and a fork of either mode MUST NOT inherit its source's A2A push configs. Replay-ness MUST be read from the run, never from the event type; the fork's own log MUST still carry the re-emitted events ([webhooks.md](webhooks.md)).
73
86
 
74
87
  **Branch.** A branch re-fires effects for sequences `>= fromSeq`; those are effects the operator asked for. A host MAY suppress branch effects and MUST NOT report that as replay suppression. A host SHOULD surface the re-fire in operator-facing fork UI.
75
88
 
76
89
  ### The effect-seam manifest
77
90
 
78
- A host advertising `replay` MUST publish `schemas/v2/effect-seam-manifest.schema.json`-shaped data at `GET /host/effect-seams` (RFC 0173 §C.1): `{ manifestVersion: "1", host: { name, build }, seams[] }`, one row per outbound effect path its node runtime can reach, `{ seam, kind, guarded: true, guardedBy, branchReFires?, note? }`. The host owns the list. The suite drives one seam of each kind it can reach and observes no re-fire (`effect-seam-manifest`, conformance.md).
91
+ A host advertising `replay` MUST publish `schemas/v2/effect-seam-manifest.schema.json`-shaped data at `GET /host/effect-seams`: `{ manifestVersion: "1", host: { name, build }, seams[] }`, one row per outbound effect path its node runtime can reach, `{ seam, kind, guarded: true, guardedBy, branchReFires?, note? }`. The host owns the list. The suite drives one seam of each kind it can reach and observes no re-fire (`effect-seam-manifest`, [conformance.md](conformance.md)).
92
+
93
+ `kind` names the outbound wire mechanism the seam leaves the host by — not the suite's driving mechanism, and not the business purpose. It MUST be one of `http`, `smtp`, `queue`, `storage`, `provider-sdk`, `webhook-fanout`, `other`.
79
94
 
80
- `kind` names the outbound **wire mechanism** the seam leaves the host by — not the suite's driving mechanism, and not the business purpose. It MUST be one of `http`, `smtp`, `queue`, `storage`, `provider-sdk`, `webhook-fanout`, `other`. Two seams that a host guards through one code path but that leave by different mechanisms are different `kind`s; two that leave by the same mechanism for different business reasons are one. `other` is the escape for a mechanism this list does not name — raw TCP, gRPC, a filesystem write, a device SDK — and a row using it MUST carry `note` naming that mechanism.
95
+ - Two seams a host guards through one code path but that leave by different mechanisms are different `kind`s; two that leave by the same mechanism for different business reasons are one.
96
+ - `other` is the escape for a mechanism this list does not name — raw TCP, gRPC, a filesystem write, a device SDK. A row using it MUST carry `note` naming that mechanism.
81
97
 
82
- **Completeness outranks driveability.** Every outbound effect path the node runtime can reach MUST be listed, including one the suite cannot drive (typically `smtp` or `other`), which the scenario records `inapplicable` naming the mechanism. A host MUST NOT omit a seam because the suite cannot drive it, and MUST NOT relabel it as a `kind` the suite can drive. The manifest is a self-declaration whose false negatives are found by audit — negative-existence, not a witness.
98
+ **Completeness outranks driveability.** Every outbound effect path the node runtime can reach MUST be listed, including one the suite cannot drive (typically `smtp` or `other`); the scenario records that one `inapplicable`, naming the mechanism. A host MUST NOT omit a seam because the suite cannot drive it, and MUST NOT relabel it as a `kind` the suite can drive. The manifest is a self-declaration whose false negatives are found by audit, not a witness.
83
99
 
84
100
  ## Replay-from-event-log internals
85
101
 
86
102
  1. Load the source run's events with `sequence < fromSeq` through the storage boundary, where an era-`2` log is translated (persistence.md).
87
103
  2. Fold them to a projected state.
88
104
  3. Initialize the new run with that state, copy-on-write into its own log.
89
- 4. For `replay`, resolve side-effecting nodes from the source run's recorded outcomes keyed on `(sourceRunId, nodeId, attempt)`; LLM invocations additionally consult the invocation log via the RFC 0041 content-addressed key.
105
+ 4. For `replay`, resolve side-effecting nodes from the source run's recorded outcomes keyed on `(sourceRunId, nodeId, attempt)`; LLM invocations additionally consult the invocation log via the content-addressed invocation key.
90
106
  5. For `branch`, executor invocations create new invocation-log entries keyed on the new `runId`.
91
107
 
92
108
  ## Forking a v1 run
93
109
 
94
- A v2 host MUST fork a run created before the cut (era `2`, persistence.md). The fork's prefix MUST be byte-equivalent to the *translated* parent — the parent as read through the codemap, not its stored bytes — and `run.started` on the fork MUST carry the legacy Subject (`issuer: urn:openwop:legacy`, identity.md) where the parent had none (RFC 0176 §A.5, scenario `fork-a-v1-run`). A backfill of an era-`2` log is permitted only atomically per run with the original preserved, so this obligation stays checkable.
110
+ A v2 host MUST fork a run created before the cut (era `2`, [persistence.md](persistence.md)). The `fork-a-v1-run` scenario witnesses this.
111
+
112
+ - The fork's prefix MUST be byte-equivalent to the *translated* parent — the parent as read through the codemap, not its stored bytes.
113
+ - `run.started` on the fork MUST carry the legacy Subject (`issuer: urn:openwop:legacy`, [identity.md](identity.md)) where the parent had none.
114
+ - A backfill of an era-`2` log is permitted only atomically per run with the original preserved, so this obligation stays checkable.
95
115
 
96
116
  ## Cross-engine ordering
97
117
 
98
- When a host advertises both `idempotency.multiRegion` and
99
- `eventLog.crossEngineOrdering`, a fork served by a region other than the one
100
- that wrote the run MUST produce the same observable state at the `fromSeq`
101
- boundary as a fork served by the writing region: `status`, `variables`, and the
102
- projected event log up to `fromSeq` MUST be byte-equivalent across regions.
103
- Per-region wall-clock and entropy fields in events AFTER the boundary MAY
104
- differ. A host that advertises one of the two and not the other keeps the
105
- single-region contract above; a host that advertises neither is single-region
106
- and the cross-region claim does not apply.
118
+ When a host advertises both `idempotency.multiRegion` and `eventLog.crossEngineOrdering`, a fork served by a region other than the one that wrote the run MUST produce the same observable state at the `fromSeq` boundary as a fork served by the writing region:
119
+
120
+ - `status`, `variables`, and the projected event log up to `fromSeq` MUST be byte-equivalent across regions.
121
+ - Per-region wall-clock and entropy fields in events after the boundary MAY differ.
122
+
123
+ A host that advertises only one of the two keeps the single-region contract above; a host that advertises neither is single-region and the cross-region claim does not apply.
107
124
 
108
125
  ## Retention
109
126
 
110
127
  A host advertising `replay` MUST document retention for source snapshots, source logs, the invocation records replay depends on, and forked runs; `retention.days` MAY advertise the window. When the range `fromSeq` needs has expired, the host MUST reject the fork with `410` or `422`; `details` SHOULD carry `sourceRunId`, `fromSeq`, and the boundary.
111
128
 
112
- See also: events.md, runs.md, persistence.md, security-defaults.md.
113
-
114
129
  ## Declared nondeterminism
115
130
 
116
- `nondeterminismPolicy` is the host's statement of which nondeterministic sources it
117
- declares rather than suppresses. A host advertising `nondeterminismPolicy.declared`
118
- MUST record every declared source in the run's event log at the point it is read, so
119
- a fork replays the recorded value rather than re-drawing it; a source the host neither
120
- declares nor suppresses is a replay defect, not a policy choice.
131
+ `nondeterminismPolicy` is the host's statement of which nondeterministic sources it declares rather than suppresses. A host advertising `nondeterminismPolicy.declared` MUST record every declared source in the run's event log at the point it is read, so a fork replays the recorded value rather than re-drawing it. A source the host neither declares nor suppresses is a replay defect, not a policy choice.
132
+
133
+ See also: events.md, runs.md, persistence.md, security-defaults.md.
121
134
 
135
+ *Sources: RFC 0036, RFC 0039, RFC 0041, RFC 0057, RFC 0104, RFC 0111, RFC 0140, RFC 0173, RFC 0176, RFC 0194.*
@@ -1,129 +1,267 @@
1
1
  # Runs
2
2
 
3
- > **Status: Stable · RFC 0170 §A, §D.1; RFC 0171 §D; RFC 0176 §B.1.**
3
+ > **Status: Stable.**
4
4
  > **Normative home:** `runList`, `limits`, `conversationPrimitive`, `dataResidency`.
5
5
 
6
6
  ## Why this exists
7
7
 
8
- A run is the unit of execution, ownership and observation. This document is the run surface of `api/v2/openapi.yaml`: how a run is created, read, streamed, cancelled, paused, forked and diffed, and the one snapshot shape every host projects from the same event log (events.md).
8
+ A run is the unit of execution, ownership and observation. This document covers the run surface of `api/v2/openapi.yaml`: how a run is created, read, streamed, cancelled, paused, forked and diffed. Every host projects the same snapshot shape from the same event log ([events.md](events.md)).
9
9
 
10
10
  ## Identity
11
11
 
12
- Every id `$ref`s `schemas/v2/ids.schema.json` (identity.md). A `runId` is tenant-bound, `<tenantId>/<opaque>`, host-minted; a host MUST reject a `runId` whose tenant segment is not the caller's with `403 id_tenant_mismatch` and MUST NOT disclose whether the run exists. A caller MUST treat every id as opaque.
12
+ Every id `$ref`s `schemas/v2/ids.schema.json` ([identity.md](identity.md)). A `runId` is host-minted and tenant-bound: `<tenantId>/<opaque>`.
13
+
14
+ - A caller MUST treat every id as opaque.
15
+ - A host MUST reject a `runId` whose tenant segment is not the caller's with `403 id_tenant_mismatch`, and MUST NOT disclose whether the run exists.
13
16
 
14
17
  ## Surface
15
18
 
16
- Every operation accepts `OpenWOP-Version` (overview.md); every mutating operation accepts `Idempotency-Key` (idempotency.md); every response carries `OpenWOP-Version`. Scopes are the `auth.md` vocabulary.
19
+ Every operation accepts `OpenWOP-Version` ([overview.md](overview.md)) and every response carries it. Every mutating operation accepts `Idempotency-Key` ([idempotency.md](idempotency.md)). Scopes are the `auth.md` vocabulary.
17
20
 
18
21
  | Operation | Method and path | Scope | Gate |
19
22
  | --- | --- | --- | --- |
20
23
  | `createRun` | `POST /runs` | `runs:create` | — |
21
24
  | `getRun` | `GET /runs/{runId}` | `runs:read` | — |
22
- | `streamRunEvents` | `GET /runs/{runId}/events` | `runs:read` | events.md |
23
- | `pollRunEvents` | `GET /runs/{runId}/events/poll` | `runs:read` | events.md |
25
+ | `listRuns` | `GET /runs` | `runs:read` | `runList` |
26
+ | `streamRunEvents` | `GET /runs/{runId}/events` | `runs:read` | [events.md](events.md) |
27
+ | `pollRunEvents` | `GET /runs/{runId}/events/poll` | `runs:read` | [events.md](events.md) |
24
28
  | `cancelRun` | `POST /runs/{runId}/cancel` | `runs:cancel` | — |
25
29
  | `bulkCancelRuns` | `POST /runs:bulk-cancel` | `runs:cancel` | — |
26
30
  | `pauseRun` | `POST /runs/{runId}:pause` | `runs:cancel` | — |
27
31
  | `resumeRun` | `POST /runs/{runId}:resume` | `runs:cancel` | — |
28
- | `forkRun` | `POST /runs/{runId}:fork` | `runs:create` + `runs:read` | `replay` (replay.md) |
29
- | `diffRun` | `GET /runs/{runId}:diff?against=` | `runs:read` on both | OPTIONAL; `404` when absent |
30
- | `getRunAncestry` | `GET /runs/{runId}/ancestry` | `runs:read` | `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported`; `404` when unadvertised |
31
- | `createAnnotation` / `listAnnotations` | `POST` / `GET /runs/{runId}/annotations` | `runs:annotate` / `runs:read` | `feedback`; `404 not_found` when unadvertised (as `getEvalSummary` and `getRunAncestry`; the registry has no 501 for an unadvertised surface) |
32
+ | `forkRun` | `POST /runs/{runId}:fork` | `runs:create` + `runs:read` | `replay` |
33
+ | `diffRun` | `GET /runs/{runId}:diff?against=` | `runs:read` on both | OPTIONAL |
34
+ | `getRunAncestry` | `GET /runs/{runId}/ancestry` | `runs:read` | `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported` |
35
+ | `createAnnotation` / `listAnnotations` | `POST` / `GET /runs/{runId}/annotations` | `runs:annotate` / `runs:read` | `feedback` |
32
36
  | `getArtifact` | `GET /runs/{runId}/artifacts/{artifactId}` | `artifacts:read` | — |
33
- | `getEvalSummary` | `GET /runs/{runId}/eval-summary` | `runs:read` | `agents.evalSuite`; `404` when unadvertised |
34
- | `getRunCompensation` | `GET /runs/{runId}/compensation` | `runs:read` | `compensation` (security-defaults.md) |
35
- | `getRunEffects` | `GET /runs/{runId}/effects` | `runs:read` | `idempotency` (idempotency.md) |
36
- | `listRuns` | `GET /runs` | `runs:read` | `runList` (RFC 0182); `404` when unadvertised |
37
+ | `getEvalSummary` | `GET /runs/{runId}/eval-summary` | `runs:read` | `agents.evalSuite` |
38
+ | `getRunCompensation` | `GET /runs/{runId}/compensation` | `runs:read` | `compensation` |
39
+ | `getRunEffects` | `GET /runs/{runId}/effects` | `runs:read` | `idempotency` |
40
+
41
+ A gated operation the host does not advertise (or an absent `diffRun`) answers `404`; for annotations it is `404 not_found`.
37
42
 
38
43
  ## Create
39
44
 
40
- The `createRun` body is closed at the composition (`unevaluatedProperties: false`): `workflowId` (REQUIRED unless `mode: eval`), `inputs`, `residency`, `tenantId`, `scopeId`, `callbackUrl` (interrupt.md §Callback delivery; a refusal is `400 validation_error`, `details.field: "callbackUrl"`), `mode`, `evalSuiteRef`, `agentId`, and the `RunOptions` fields `configurable`, `tags`, `metadata`. A body without `RunOptions` MUST be accepted as if it were `{}`.
45
+ The `createRun` body is closed (`unevaluatedProperties: false`). Its fields:
46
+
47
+ - `workflowId` — REQUIRED unless `mode: eval`.
48
+ - `inputs`, `residency`, `tenantId`, `scopeId`.
49
+ - `callbackUrl` — see [interrupt.md](interrupt.md) §Callback delivery. A refused value is `400 validation_error` with `details.field: "callbackUrl"`.
50
+ - `mode`, `evalSuiteRef`, `agentId`.
51
+ - The `RunOptions` fields `configurable`, `tags`, `metadata`. A body without `RunOptions` MUST be accepted as if it were `{}`.
52
+
53
+ ### Request headers
41
54
 
42
55
  | Header | Rule |
43
56
  | --- | --- |
44
- | `Idempotency-Key` | RECOMMENDED; a replayed create MUST NOT create a second run and carries `OpenWOP-Idempotent-Replay: true`. |
57
+ | `Idempotency-Key` | RECOMMENDED. A replayed create MUST NOT create a second run, and carries `OpenWOP-Idempotent-Replay: true`. |
45
58
  | `OpenWOP-Dedup: enforce` | The host MUST reject a duplicate `(tenantId, scopeId)` with `409 run_already_active` and `Retry-After`. |
46
- | `OpenWOP-Force-Engine-Version` | Test keys only; the seams profile. A host MUST reject it on a production credential with `403`. |
59
+ | `OpenWOP-Force-Engine-Version` | Test keys only (the seams profile). A host MUST reject it on a production credential with `403`. |
60
+
61
+ ### Response
62
+
63
+ The `201` response is `{ runId, status, eventsUrl, statusUrl? }`. `status` is one of `pending`, `running`, `waiting-approval`, `waiting-input`, `waiting-external`.
47
64
 
48
- The `201` response is `{ runId, status, eventsUrl, statusUrl? }`, `status` one of `pending`, `running`, `waiting-approval`, `waiting-input`, `waiting-external`. `eventsUrl` and `statusUrl` MUST resolve under the origin the request was made to — a relative path, or an absolute URL on the same origin — and MUST NOT downgrade the scheme; a link that names a different host (a backing service behind the origin) or `http://` on an `https://` origin is non-conformant. The base that minted `runId` MUST resolve it: `GET /runs/{runId}` and `GET /runs/{runId}/events/poll` at that base MUST answer `200` for the id the `201` returned, percent-encoded per identity.md §5 (a front door that decodes `%2F` before routing makes every tenant-bound id unreachable). `mode: eval` (with `evalSuiteRef` and `agentId` REQUIRED) starts an eval-suite projection that emits the content-free `eval.*` family and terminates with an `EvalSummary`; a host that does not advertise `agents.evalSuite` MUST reject it. A host advertising `dataResidency` MUST reject a `residency.region` outside `dataResidency.regions` with `422 residency_unavailable` and create no run (§"Conversation and residency capabilities"). A workflow that references a capability-gated reserved node type on a host that does not advertise the capability MUST be rejected with `422 capability_required`.
65
+ - `eventsUrl` and `statusUrl` MUST resolve under the origin the request was made to — a relative path, or an absolute URL on the same origin — and MUST NOT downgrade the scheme. A link naming a different host, or `http://` on an `https://` origin, is non-conformant.
66
+ - The base that minted `runId` MUST resolve it: `GET /runs/{runId}` and `GET /runs/{runId}/events/poll` at that base MUST answer `200` for the returned id, percent-encoded per [identity.md](identity.md) §5. (A front door that decodes `%2F` before routing makes every tenant-bound id unreachable.)
49
67
 
50
- `run.started` (events.md) MUST echo the run's `owner` block exactly as `RunSnapshot.owner` carries it (RFC 0170 §A.1); `transport` records `rest`, `mcp`, `a2a` or `ui`.
68
+ ### Refusals
69
+
70
+ - `mode: eval` makes `evalSuiteRef` and `agentId` REQUIRED. It starts an eval-suite projection that emits the content-free `eval.*` family and terminates with an `EvalSummary`. A host that does not advertise `agents.evalSuite` MUST reject it.
71
+ - A host advertising `dataResidency` MUST reject a `residency.region` outside `dataResidency.regions` with `422 residency_unavailable` and create no run (see §"Conversation and residency capabilities").
72
+ - A workflow that references a capability-gated reserved node type on a host that does not advertise the capability MUST be rejected with `422 capability_required`.
73
+
74
+ ### The start event
75
+
76
+ `run.started` ([events.md](events.md)) MUST echo the run's `owner` block exactly as `RunSnapshot.owner` carries it. Its `transport` records `rest`, `mcp`, `a2a` or `ui`.
51
77
 
52
78
  ## Run options
53
79
 
54
80
  `schemas/v2/run-options.schema.json` is `{ configurable?, tags?, metadata? }`.
55
81
 
56
- `configurable` is `schemas/v2/configurable.schema.json`: closed, nested and versioned (RFC 0171 §D.1). The request body `$ref`s it directly; there is no `allOf`-merge of an open map. `version` is REQUIRED and is `1`.
82
+ `configurable` is `schemas/v2/configurable.schema.json`: closed, nested and versioned. The request body `$ref`s it directly; there is no `allOf`-merge of an open map. `version` is REQUIRED and is `1`. It has five sections:
83
+
84
+ | Section | Keys |
85
+ | --- | --- |
86
+ | `run` | `recursionLimit`, `runTimeoutMs`, `maxLoopIterations`, `escalationThreshold` |
87
+ | `ai` | `provider`, `model`, `temperature` (0..2), `maxTokens`, `credentialRef`, `promptOverrides`, `mockProvider`, `reasoningVerbosity` (`none` \| `summary` \| `full`), `maxRefusals` |
88
+ | `distillation` | `tokenBudget` |
89
+ | `budget` | `schemas/v2/budget-policy.schema.json` — the run's budget policy |
90
+ | `extensions` | `<org>: {…}` — a vendor key lives under its registered org and nowhere else |
91
+
92
+ ### `run` section
57
93
 
58
- | Section | Keys | Rule |
59
- | --- | --- | --- |
60
- | `run` | `recursionLimit`, `runTimeoutMs`, `maxLoopIterations`, `escalationThreshold` | `recursionLimit` is clamped to `limits.maxNodeExecutions`. `runTimeoutMs` resolves to `min(runTimeoutMs, limits.maxRunDurationMs)`; an out-of-range value MUST return `400 validation_error` at create, and a breach MUST emit `cap.breached { kind: 'run-duration' }` and terminate the run `failed` with `run_timeout`. `maxLoopIterations` resolves against `limits.maxLoopIterations`; a breach MUST emit `cap.breached { kind: 'loop-iterations' }` and fail with `loop_limit_exceeded`. `escalationThreshold` is the `low-confidence` threshold (interrupt.md). |
61
- | `ai` | `provider`, `model`, `temperature` (0..2), `maxTokens`, `credentialRef`, `promptOverrides`, `mockProvider`, `reasoningVerbosity` (`none` \| `summary` \| `full`), `maxRefusals` | `provider` MUST be in `aiProviders.providers`, else `400 validation_error`. `credentialRef` MUST reference a provider in `aiProviders.byok`, else `403 credential_forbidden`; it never carries key material. `mockProvider` is test-keys-only: a host MUST refuse it on a production credential with `403`. `maxRefusals` is the refusal ceiling (events.md E5). |
62
- | `distillation` | `tokenBudget` | Resolves to `min(tokenBudget, memory.distillation.maxTokenBudget)`; a run that cannot distill within it MUST fail atomically with `token_budget_exceeded`. |
63
- | `budget` | `schemas/v2/budget-policy.schema.json` | The run's budget policy. |
64
- | `extensions` | `<org>: {…}` | A vendor key lives under its registered org and nowhere else. |
94
+ - `recursionLimit` is clamped to `limits.maxNodeExecutions`.
95
+ - `runTimeoutMs` resolves to `min(runTimeoutMs, limits.maxRunDurationMs)`. An out-of-range value MUST return `400 validation_error` at create. A breach MUST emit `cap.breached { kind: 'run-duration' }` and terminate the run `failed` with `run_timeout`.
96
+ - `maxLoopIterations` resolves against `limits.maxLoopIterations`. A breach MUST emit `cap.breached { kind: 'loop-iterations' }` and fail with `loop_limit_exceeded`.
97
+ - `escalationThreshold` is the `low-confidence` threshold ([interrupt.md](interrupt.md)).
65
98
 
66
- An unknown root key, an unknown key inside a section, or a dotted key (`ai.provider` as a string key) MUST be rejected with `400 validation_error`. A host MUST persist `RunOptions` on the run at creation, MUST surface the same `configurable` to every attempt of a node, and MUST NOT allow `configurable` to change after creation. A workflow's `configurableSchema` MUST be validated against at create time and MUST be surfaced on `getWorkflow`.
99
+ ### `ai` section
67
100
 
68
- `tags` is an opaque string array (at most 100 entries, each at most 256 characters, valid UTF-8); a host MUST NOT reject a tag on format and MUST return `400 validation_error` over the limits. `metadata` is a free-form JSON object the engine MUST NOT consume for any execution decision; a host MUST persist it. Both surface unchanged on `RunSnapshot`.
101
+ - `provider` MUST be in `aiProviders.providers`, else `400 validation_error`.
102
+ - `credentialRef` MUST reference a provider in `aiProviders.byok`, else `403 credential_forbidden`. It never carries key material.
103
+ - `mockProvider` is test-keys-only: a host MUST refuse it on a production credential with `403`.
104
+ - `maxRefusals` is the refusal ceiling ([events.md](events.md) E5).
105
+
106
+ ### `distillation` section
107
+
108
+ `tokenBudget` resolves to `min(tokenBudget, memory.distillation.maxTokenBudget)`. A run that cannot distill within it MUST fail atomically with `token_budget_exceeded`.
109
+
110
+ ### Validation and persistence
111
+
112
+ - An unknown root key, an unknown key inside a section, or a dotted key (`ai.provider` as a string key) MUST be rejected with `400 validation_error`.
113
+ - A host MUST persist `RunOptions` on the run at creation, MUST surface the same `configurable` to every attempt of a node, and MUST NOT allow `configurable` to change after creation.
114
+ - A workflow's `configurableSchema` MUST be validated against at create time and MUST be surfaced on `getWorkflow`.
115
+
116
+ ### `tags` and `metadata`
117
+
118
+ - `tags` is an opaque string array: at most 100 entries, each at most 256 characters, valid UTF-8. A host MUST NOT reject a tag on format, and MUST return `400 validation_error` over the limits.
119
+ - `metadata` is a free-form JSON object. A host MUST persist it, and the engine MUST NOT consume it for any execution decision.
120
+ - Both surface unchanged on `RunSnapshot`.
69
121
 
70
122
  ## Snapshot
71
123
 
72
- `getRun` returns `schemas/v2/run-snapshot.schema.json`, the fold of the event log through the run projection. `runId`, `workflowId`, `status`, `owner` and `eventLogSchemaVersion` are REQUIRED; the object is closed.
124
+ `getRun` returns `schemas/v2/run-snapshot.schema.json`: the fold of the event log through the run projection. The object is closed. `runId`, `workflowId`, `status`, `owner` and `eventLogSchemaVersion` are REQUIRED.
73
125
 
74
- | Field | Rule |
126
+ | Field | Meaning |
75
127
  | --- | --- |
76
- | `owner` | `{ tenant, workspace?, subject }`, closed, `subject` REQUIRED (`schemas/v2/subject.schema.json`). `principal` and `principalKind` do not exist. A run created before the host emitted subjects reads with the legacy subject rule (identity.md), stamped at first read and never rewritten. |
77
- | `status` | `pending`, `running`, `paused`, `waiting-approval`, `waiting-input`, `waiting-external`, `completed`, `failed`, `cancelling`, `cancelled`. `waiting-external` MUST be used when the suspended interrupt's `kind` is `external-event`. `cancelling` is the state between an accepted cancel and the terminal `cancelled`. The vocabulary grows by overview.md §0. |
78
- | `eventLogSchemaVersion` | The era key, integer ≥ 2 (persistence.md §"The era key"). |
79
- | `engineVersion` | Integer. |
80
- | `compensationStatus` | `none`, `pending`, `running`, `completed`, `partial`, `failed`, `manual`. A host that does not advertise `compensation` MUST omit it; a host that does MUST include it on every snapshot, `none` when never requested. |
81
- | `currentNodeId` | Set while suspended; names the node holding the interrupt. |
82
- | `error` | `{ code, message, details? }` on terminal `failed`. |
83
- | `configurable`, `tags`, `metadata` | The persisted `RunOptions`. |
84
- | `agent`, `runOrchestrator` | `schemas/v2/agent-ref.schema.json`; `runOrchestrator` MUST NOT change for the run's lifetime. |
85
- | `metrics.openwopCost` | `{ usd, tokens { input, output }, model, provider, duration_ms }`; absence is not zero. |
86
-
87
- The `200` SHOULD carry a strong `ETag` derived from the latest persisted `sequence`; when present it MUST change on every observable transition and be stable otherwise. A request whose `If-None-Match` matches MUST receive `304` with no body. A host MAY compress (`gzip` baseline; `br`, `zstd` only where advertised under `restTransport.contentEncodings`) and MUST then set `Content-Encoding` and `Vary: Accept-Encoding`; the decoded body is byte-identical.
128
+ | `owner` | `{ tenant, workspace?, subject }`, closed; `subject` REQUIRED (`schemas/v2/subject.schema.json`) |
129
+ | `status` | Run state (below) |
130
+ | `eventLogSchemaVersion` | The era key, integer ≥ 2 ([persistence.md](persistence.md) §"The era key") |
131
+ | `engineVersion` | Integer |
132
+ | `compensationStatus` | `none`, `pending`, `running`, `completed`, `partial`, `failed`, `manual` |
133
+ | `currentNodeId` | Set while suspended; names the node holding the interrupt |
134
+ | `error` | `{ code, message, details? }` on terminal `failed` |
135
+ | `configurable`, `tags`, `metadata` | The persisted `RunOptions` |
136
+ | `agent`, `runOrchestrator` | `schemas/v2/agent-ref.schema.json` |
137
+ | `metrics.openwopCost` | `{ usd, tokens { input, output }, model, provider, duration_ms }`; absence is not zero |
138
+
139
+ Field rules:
140
+
141
+ - `status` is one of `pending`, `running`, `paused`, `waiting-approval`, `waiting-input`, `waiting-external`, `completed`, `failed`, `cancelling`, `cancelled`. `waiting-external` MUST be used when the suspended interrupt's `kind` is `external-event`. `cancelling` is the state between an accepted cancel and the terminal `cancelled`. The vocabulary grows by [overview.md](overview.md) §0.
142
+ - `owner`: a run created before the host emitted subjects reads with the subject rule in [identity.md](identity.md), stamped at first read and never rewritten.
143
+ - `compensationStatus`: a host that does not advertise `compensation` MUST omit it. A host that does MUST include it on every snapshot, `none` when never requested.
144
+ - `runOrchestrator` MUST NOT change for the run's lifetime.
145
+
146
+ ### Caching and encoding
147
+
148
+ - The `200` SHOULD carry a strong `ETag` derived from the latest persisted `sequence`. When present it MUST change on every observable transition and be stable otherwise.
149
+ - When the host sends an `ETag`, a request whose `If-None-Match` matches it MUST receive `304` with no body.
150
+ - A host MAY compress (`gzip` baseline; `br` and `zstd` only where advertised under `extensions["<org>.rest-transport"].contentEncodings`, [ext/restTransport](../ext/restTransport/README.md)). It MUST then set `Content-Encoding` and `Vary: Accept-Encoding`. The decoded body is byte-identical.
88
151
 
89
152
  ## List
90
153
 
91
- `GET /runs` (RFC 0182; gated on `runList`) returns `{ runs: RunSnapshot[], nextCursor? }`, the caller's runs newest first: only runs whose tenant segment is the caller's, every `runId` bound (identity.md §5), and a run the caller created MUST appear. A page MUST NOT exceed `runList.maxPageSize`; `cursor` is opaque and one the host did not mint MUST be refused `400 validation_error`; `workflowId` and `status` are exact-match filters when `runList.filters` names them (an unadvertised filter is ignored).
154
+ `GET /runs` (gated on `runList`) returns `{ runs: RunSnapshot[], nextCursor? }`: the caller's runs, newest first.
155
+
156
+ - Only runs whose tenant segment is the caller's appear, and every `runId` is bound ([identity.md](identity.md) §5). A run the caller created MUST appear.
157
+ - A page MUST NOT exceed `runList.maxPageSize`.
158
+ - `cursor` is opaque. A cursor the host did not mint MUST be refused with `400 validation_error`.
159
+ - `workflowId` and `status` are exact-match filters when `runList.filters` names them. An unadvertised filter is ignored.
92
160
 
93
161
  ## Cancel
94
162
 
95
- `cancelRun` accepts `{ reason? }` and answers `200 { runId, status }` with `status` `cancelling` or `cancelled`; the cascade MAY be asynchronous and the run emits `run.cancelled` when it completes. A cancel on a run that is already terminal (`completed`, `failed`, `cancelled`) MUST be refused `409 run_terminal`; a `200` whose `status` echoes the terminal state is outside this grammar. Cancelling a parent MUST NOT silently abandon an active compensation (security-defaults.md). `run.cancelled.parentRunId` with `reason: parent-cancelled` records a cascade from a parent.
163
+ `cancelRun` accepts `{ reason? }` and answers `200 { runId, status }`, where `status` is `cancelling` or `cancelled`. The cascade MAY be asynchronous; the run emits `run.cancelled` when it completes.
164
+
165
+ - A cancel on a terminal run (`completed`, `failed`, `cancelled`) MUST be refused with `409 run_terminal`. A `200` echoing the terminal state is non-conformant.
166
+ - Cancelling a parent MUST NOT silently abandon an active compensation ([security-defaults.md](security-defaults.md)).
167
+ - `run.cancelled.parentRunId` with `reason: parent-cancelled` records a cascade from a parent.
168
+
169
+ ### Bulk cancel
170
+
171
+ `bulkCancelRuns` accepts `{ runIds[1..100], reason? }`.
172
+
173
+ - Over the host's cap (RECOMMENDED 100) it MUST return `400 validation_error` with `details.maxRunIds`.
174
+ - The host MUST process each id independently, and MUST return `200 { results[] }` in request order, even when every id failed.
175
+ - The host MUST enforce authorization per id. A run the caller cannot see yields `ok: false` with an error envelope in that entry, never a top-level `403`.
176
+
177
+ Per-entry errors ([errors.md](errors.md)):
96
178
 
97
- A non-terminal run inherited from v1 continues, or is cancelled `v1_pin_unsupported`, per persistence.md §"Runs pinned to v1" (RFC 0176 §B.1).
179
+ | Condition | Code |
180
+ | --- | --- |
181
+ | The id's tenant segment is not the caller's ([identity.md](identity.md) §5 applies inside an entry as on a path) | `id_tenant_mismatch`, or `not_found` where existence is not leaked |
182
+ | A run in the caller's tenant the caller may not cancel | `run_forbidden` |
183
+ | A run already terminal | `run_terminal` |
98
184
 
99
- `bulkCancelRuns` accepts `{ runIds[1..100], reason? }`; over the host's cap (RECOMMENDED 100) it MUST return `400 validation_error` with `details.maxRunIds`. The host MUST process each id independently, MUST return `200 { results[] }` in request order even when every id failed, and MUST enforce authorization per id: a run the caller cannot see yields `ok: false` with an error envelope in that entry, never a top-level `403` — `id_tenant_mismatch` (or `not_found` where existence is not leaked) when the id's tenant segment is not the caller's (identity.md §5 applies inside an entry exactly as on a path), `run_forbidden` for a run in the caller's tenant the caller may not cancel, `run_terminal` for a run already terminal. `ok: true` carries `status` `cancelling` or `cancelled`; `ok: false` carries the error envelope (errors.md).
185
+ `ok: true` carries `status` `cancelling` or `cancelled`; `ok: false` carries the error envelope.
100
186
 
101
187
  ## Pause and resume
102
188
 
103
- `pauseRun` accepts `{ reason?, drainPolicy? }` with `drainPolicy` `immediate` (snapshot between events) or `drain-current-node` (default; the executing node reaches a terminal first) and answers `202 { runId, status: 'paused', pausedAt? }`; the transition emits `run.paused`, whose payload echoes the request's `drainPolicy` word. With `immediate`, the attempt that was executing is cut between events: it has no terminal node event, a host MUST NOT record `node.failed` (or any terminal node event) for it, and the resumed run's `node.started` begins a fresh attempt. The record of the interruption is `run.paused` itself; its payload MAY carry `interruptedNodeId` and `interruptedAttempt` so a `debug` consumer can see which attempt was cut. A run already paused, terminal, or otherwise unpausable MUST receive `409`: `run_terminal` when the run is terminal, else `run_state_conflict` with `details.runStatus` naming the status that refused it. `resumeRun` accepts `{ reason? }`, answers `202 { runId, status: 'running', resumedAt? }`, emits `run.resumed`, and MUST return `409` when the run is not paused — `run_terminal` or `run_state_conflict` by the same rule. Only `resumeRun` or a cancel exits `paused`. A replay MUST fold `run.paused` and `run.resumed` as no-ops for projected state.
189
+ `pauseRun` accepts `{ reason?, drainPolicy? }` and answers `202 { runId, status: 'paused', pausedAt? }`. The transition emits `run.paused`, whose payload echoes the request's `drainPolicy` word.
190
+
191
+ `drainPolicy` is one of:
192
+
193
+ - `drain-current-node` (default) — the executing node reaches a terminal first.
194
+ - `immediate` — the run is snapshotted between events. The executing attempt is cut: it has no terminal node event, a host MUST NOT record `node.failed` (or any terminal node event) for it, and the resumed run's `node.started` begins a fresh attempt. `run.paused` itself records the interruption; its payload MAY carry `interruptedNodeId` and `interruptedAttempt` so a `debug` consumer can see which attempt was cut.
195
+
196
+ `resumeRun` accepts `{ reason? }`, answers `202 { runId, status: 'running', resumedAt? }`, and emits `run.resumed`.
197
+
198
+ Rules:
199
+
200
+ - A pause on a run that is already paused, terminal, or otherwise unpausable MUST receive `409`.
201
+ - A resume on a run that is not paused MUST return `409`.
202
+ - In both cases the code is `run_terminal` when the run is terminal, else `run_state_conflict` with `details.runStatus` naming the refusing status.
203
+ - Only `resumeRun` or a cancel exits `paused`.
204
+ - A replay MUST fold `run.paused` and `run.resumed` as no-ops for projected state.
104
205
 
105
206
  ## Fork
106
207
 
107
- `forkRun` accepts `{ mode: replay | branch, fromSeq?, runOptionsOverlay? }`: events with `sequence < fromSeq` are fixed history and events `≥ fromSeq` re-execute. `fromSeq` is REQUIRED for `branch` and defaults to `0` for `replay`; `runOptionsOverlay` is `branch`-only, and a `replay` with a non-empty overlay MUST be rejected with `400`. A `fromSeq` not in the source log MUST be rejected with `422 fork_point_invalid`. The `201` response is `{ runId, sourceRunId, fromSeq?, mode, status, eventsUrl }`. The child's `owner` is copied verbatim from the parent (RFC 0170 §A.4). Determinism, side-effect suppression, and forking an era-2 parent are in replay.md.
208
+ `forkRun` accepts `{ mode: replay | branch, fromSeq?, runOptionsOverlay? }`. Events with `sequence < fromSeq` are fixed history; events `≥ fromSeq` re-execute.
209
+
210
+ - `fromSeq` is REQUIRED for `branch` and defaults to `0` for `replay`.
211
+ - `runOptionsOverlay` is `branch`-only. A `replay` with a non-empty overlay MUST be rejected with `400`.
212
+ - A `fromSeq` not in the source log MUST be rejected with `422 fork_point_invalid`.
213
+
214
+ The `201` response is `{ runId, sourceRunId, fromSeq?, mode, status, eventsUrl }`. The child's `owner` is copied verbatim from the parent. Determinism, side-effect suppression, and forking an era-2 parent are in [replay.md](replay.md).
108
215
 
109
216
  ## Diff and ancestry
110
217
 
111
- `diffRun` returns `schemas/v2/run-diff-response.schema.json`: `divergedAtSeq`, ordered `eventDiffs[]`, `stateDiff`, optional `truncated`. The diff MUST be a pure function of the two logs: identical logs MUST yield `divergedAtSeq: null` and empty `eventDiffs`; `eventId`, `runId`, `timestamp` and other run-scoped fields MUST be excluded from comparison. A host that diffs an in-flight prefix MUST set `truncated: true`. A caller lacking `runs:read` on either run MUST receive `403`. `getRunAncestry` returns `schemas/v2/run-ancestry-response.schema.json` (`runId`, `hostId`, `parent` or `null`); a client walks the chain one hop at a time via `parent.wellKnownUrl`.
218
+ `diffRun` returns `schemas/v2/run-diff-response.schema.json`: `divergedAtSeq`, ordered `eventDiffs[]`, `stateDiff`, optional `truncated`.
219
+
220
+ - The diff MUST be a pure function of the two logs. Identical logs MUST yield `divergedAtSeq: null` and empty `eventDiffs`.
221
+ - `eventId`, `runId`, `timestamp` and other run-scoped fields MUST be excluded from comparison.
222
+ - A host that diffs an in-flight prefix MUST set `truncated: true`.
223
+ - A caller lacking `runs:read` on either run MUST receive `403`.
224
+
225
+ `getRunAncestry` returns `schemas/v2/run-ancestry-response.schema.json` (`runId`, `hostId`, `parent` or `null`). A client walks the chain one hop at a time via `parent.wellKnownUrl`.
112
226
 
113
227
  ## Annotations, artifacts, eval summary
114
228
 
115
- `createAnnotation` accepts `schemas/v2/annotation-create.schema.json` and returns `201` with `schemas/v2/annotation.schema.json`; `listAnnotations` returns `{ annotations[] }`. An annotation is a live notification (`run.annotated`), never a run event: it MUST NOT enter the event log and MUST be excluded from fork, replay and diff. `getArtifact` answers `application/json` with an implementation-defined object or, when `Accept` prefers `application/a2a+json`, an A2A `Artifact` (`schemas/v2/artifact.schema.json`); a host SHOULD offer the latter. A body served as `application/a2a+json` MUST validate against that schema with `artifactId` equal to the path's, and a `url` Part in it MUST NOT resolve beyond the caller's `artifacts:read` authorization. `getEvalSummary` returns `schemas/v2/eval-summary.schema.json` for a terminal eval run, `409` while it is running, `404` when the run is not an eval run; the summary MUST be content-free of task output, rubric prose and credentials.
229
+ ### Annotations
230
+
231
+ `createAnnotation` accepts `schemas/v2/annotation-create.schema.json` and returns `201` with `schemas/v2/annotation.schema.json`. `listAnnotations` returns `{ annotations[] }`.
232
+
233
+ An annotation is a live notification (`run.annotated`), never a run event. It MUST NOT enter the event log and MUST be excluded from fork, replay and diff.
234
+
235
+ ### Artifacts
236
+
237
+ `getArtifact` answers `application/json` with an implementation-defined object or, when `Accept` prefers `application/a2a+json`, an A2A `Artifact` (`schemas/v2/artifact.schema.json`). A host SHOULD offer the latter.
238
+
239
+ - A body served as `application/a2a+json` MUST validate against that schema, with `artifactId` equal to the path's.
240
+ - A `url` Part in it MUST NOT resolve beyond the caller's `artifacts:read` authorization.
241
+
242
+ ### Eval summary
243
+
244
+ `getEvalSummary` returns `schemas/v2/eval-summary.schema.json` for a terminal eval run, `409` while it is running, and `404` when the run is not an eval run. The summary MUST be free of task output, rubric prose and credentials.
116
245
 
117
246
  ## Conversation and residency capabilities
118
247
 
119
- `conversationPrimitive` carries no payload: its presence is the claim (capabilities.md §2). A
120
- workflow whose `nodes[].typeId` references `core.conversationGate` MUST be refused by a host that
121
- does not advertise `conversationPrimitive`, at registration or at run creation, with `422
122
- capability_required` naming the family in `details.requiredCapability`.
248
+ ### `conversationPrimitive`
249
+
250
+ `conversationPrimitive` carries no payload: its presence is the claim ([capabilities.md](capabilities.md) §2). A host that does not advertise it MUST refuse a workflow whose `nodes[].typeId` references `core.conversationGate` — at registration or at run creation — with `422 capability_required`, naming the family in `details.requiredCapability`.
251
+
252
+ A conversation turn MAY carry `parts`: a non-empty array of A2A `Part` objects (`schemas/v2/part.schema.json`) that marks the turn A2A-shaped. A producer SHOULD emit it and keep `content` readable by consumers that predate it. A turn without `parts` stays valid on emission, replay and fork.
253
+
254
+ ### `dataResidency`
255
+
256
+ A host advertising `dataResidency` MUST honor-or-reject, and MUST NOT silently accept-and-ignore:
257
+
258
+ - accept a `residency` constraint naming a region in `dataResidency.regions`;
259
+ - refuse one it does not advertise with `residency_unavailable`.
260
+
261
+ A host that does not advertise `dataResidency` MAY ignore or reject a `residency` constraint, but MUST NOT claim to honor it.
262
+
263
+ ## During the v1 overlap
123
264
 
124
- A conversation turn MAY carry `parts`, a non-empty array of A2A `Part` objects (`schemas/v2/part.schema.json`); its presence marks the turn A2A-shaped. A producer SHOULD emit it and keep `content` readable by consumers that predate it. A turn without `parts` stays valid on emission, replay and fork.
265
+ A non-terminal run inherited from v1 continues, or is cancelled `v1_pin_unsupported`, per [persistence.md](persistence.md) §"Runs pinned to v1".
125
266
 
126
- A host advertising `dataResidency` MUST honor-or-reject: accept a `residency` constraint naming a
127
- region in `dataResidency.regions`, refuse one it does not advertise with `residency_unavailable`,
128
- and MUST NOT silently accept-and-ignore. A host that does not advertise `dataResidency` MAY ignore or
129
- reject a `residency` constraint but MUST NOT claim to honor it.
267
+ *Sources: RFC 0170, RFC 0171, RFC 0176, RFC 0182.*