|
| 1 | +# LEARNED |
| 2 | + |
| 3 | +Notes from writing the fourteen examples against RFC-0001 v2. Items |
| 4 | +roughly ordered by how often they bit. Some are real spec gaps; |
| 5 | +some are friction in expressing the spec in Python. |
| 6 | + |
| 7 | +## Spec gaps and ambiguities |
| 8 | + |
| 9 | +### 1. `extensions.optional` is convention, not schema |
| 10 | + |
| 11 | +§21.3 says receivers MUST nack unknowns *unless* the sender marked |
| 12 | +the message `extensions.optional: true`. The envelope schema |
| 13 | +documents `extensions` as a free-form `dict[str, Any]` but never |
| 14 | +reserves the `optional` key. Two examples (`extensions`, |
| 15 | +`reasoning_streams`) lean on this convention. Spec should reserve |
| 16 | +the key or define a typed envelope flag. |
| 17 | + |
| 18 | +### 2. Backfill terminator envelope is unspecified |
| 19 | + |
| 20 | +§13.3 prescribes "a synthetic `subscription.backfill_complete` |
| 21 | +marker" but doesn't pin the envelope type. We adopt |
| 22 | +`event.emit` with `payload.name = "subscription.backfill_complete"`. |
| 23 | +`resumability` relies on this to know when replay ends. |
| 24 | + |
| 25 | +### 3. Lease `resource` grammar is free-form |
| 26 | + |
| 27 | +§15.5 defines `resource` as a string; the examples drift to |
| 28 | +different conventions: |
| 29 | + |
| 30 | +- `leases`: `host:<host>/<binary>/<argv1>` |
| 31 | +- `lease_revocation`: `table:<schema>.<name>` |
| 32 | +- `permission_challenge`: `ticket:<id>/<patch_sha>` |
| 33 | + |
| 34 | +All reasonable. None interoperate. A loose grammar |
| 35 | +(`<scheme>:<path>`) would help observability sinks slice by |
| 36 | +resource type. |
| 37 | + |
| 38 | +### 4. Handoff context transfer is gestural |
| 39 | + |
| 40 | +§14 mentions `shared_memory_ref` in an example payload but |
| 41 | +`AgentHandoffPayload` only models `target_runtime`, `job_id`, |
| 42 | +`session_id`. `handoff` packs the conversation into an |
| 43 | +`artifact.put` and references it from a non-modeled |
| 44 | +`shared_memory_ref` field. Either formalize the field or specify |
| 45 | +that handoff context flows exclusively as artifacts. |
| 46 | + |
| 47 | +### 5. Capability extensions need a value type |
| 48 | + |
| 49 | +§7 lets capabilities carry arbitrary additional fields (the SDK's |
| 50 | +`Capabilities` model has `extra="allow"`), but §21 only addresses |
| 51 | +extension *messages*, not extension *capability values*. |
| 52 | +`capability_negotiation` advertises `arcpx.market.cost_per_mtok.v1` as |
| 53 | +a numeric capability value. The spec should explicitly cover this. |
| 54 | + |
| 55 | +### 6. Cooperative cancel contract is loose |
| 56 | + |
| 57 | +§10.4 says "the runtime SHOULD drive the target to a clean |
| 58 | +checkpoint within `deadline_ms` before terminating" but leaves |
| 59 | +escalation to "MAY hard-kill". Implementations will diverge on |
| 60 | +whether `deadline_ms` resets on each progress event or is absolute. |
| 61 | +`cancellation` documents the contract but can't enforce it. |
| 62 | + |
| 63 | +### 7. `permission.request` reply envelope is implicit |
| 64 | + |
| 65 | +The §15.4 example shows `permission.grant` / `permission.deny` but |
| 66 | +the spec also implies `lease.granted` follows on grant. Whether the |
| 67 | +*reply* to a `permission.request` is `permission.grant` (with |
| 68 | +`lease.granted` arriving separately) or `lease.granted` directly is |
| 69 | +implementation-defined. The SDK collapses these; `leases`, |
| 70 | +`lease_revocation`, and `permission_challenge` all treat |
| 71 | +`lease.granted` as the success reply. Worth pinning. |
| 72 | + |
| 73 | +### 8. Mirror role is between Observer and Peer |
| 74 | + |
| 75 | +§5 defines Observers as "subscriptions only; never command" and |
| 76 | +Peer Runtimes as the federation/`agent.delegate` party. The mirror |
| 77 | +in `reasoning_streams` does both: it subscribes to the primary's |
| 78 | +thoughts AND emits `agent.delegate` carrying critiques back into |
| 79 | +the primary's session. We classify it as a peer runtime |
| 80 | +(`trust_level: trusted`); §5 is silent on this hybrid. |
| 81 | + |
| 82 | +## Python expression friction |
| 83 | + |
| 84 | +### 9. `client.events()` is single-consumer |
| 85 | + |
| 86 | +`ARCPClient.events()` exposes a single shared queue. Multiple |
| 87 | +coroutines iterating it in parallel starve each other. `delegation` |
| 88 | +ships a `JobMux` that owns the iterator and demuxes by `job_id`. |
| 89 | +The SDK should ship something like this, or at least document the |
| 90 | +constraint. |
| 91 | + |
| 92 | +### 10. Boilerplate before `client.envelope()` was painful |
| 93 | + |
| 94 | +Pre-helper, every example had its own `_msg_id()` and threaded |
| 95 | +`session_id` by hand. We added `ARCPClient.envelope(type, ...)` to |
| 96 | +the SDK partway through; it cut ~6 LOC per call site. If the SDK |
| 97 | +hadn't grown this method these examples would be 30% noise. |
| 98 | + |
| 99 | +### 11. Reasoning streams want stronger typing |
| 100 | + |
| 101 | +`stream.chunk.payload` is `extra="allow"` because chunk shape |
| 102 | +varies by `kind`. For `kind: thought` we end up with a hand-rolled |
| 103 | +contract (`role: assistant_thought`, `content: str`, |
| 104 | +`redacted: bool`). A nested model per kind would be friendlier to |
| 105 | +write tests against. |
| 106 | + |
| 107 | +### 12. `idempotency_key` retention horizon is unstated |
| 108 | + |
| 109 | +§6.4 says runtimes "SHOULD persist `(session_principal, |
| 110 | +idempotency_key)` for at least the lease horizon of the operation", |
| 111 | +but `resumability` and `heartbeats` use |
| 112 | +`idempotency_key` for things that aren't lease-scoped (workflow |
| 113 | +steps, worker re-dispatch). A "MUST persist for at least the |
| 114 | +declared retention window" clause would settle this. |
| 115 | + |
| 116 | +### 13. Unbounded `asyncio.create_task` for runtime-of-process tasks |
| 117 | + |
| 118 | +Several examples spawn supervisor / drain / route tasks that live |
| 119 | +for `main()`'s whole lifetime. Ruff's RUF006 wants the task stored |
| 120 | +to a name; we suppress because storing in a never-read variable is |
| 121 | +weirder than just letting the task run. This isn't an ARCP issue — |
| 122 | +it's a Python-async one — but writing this many event loops back to |
| 123 | +back made it loud. |
| 124 | + |
| 125 | +## What I'd change in the spec |
| 126 | + |
| 127 | +- Reserve `extensions.optional: bool` in §21.3. |
| 128 | +- Pin the backfill terminator envelope in §13.3. |
| 129 | +- Sketch a loose `<scheme>:<path>` lease `resource` grammar. |
| 130 | +- Either model `shared_memory_ref` on `AgentHandoffPayload` or |
| 131 | + state the artifact-only convention in §14. |
| 132 | +- Cover capability extension *values* in §21. |
| 133 | +- Clarify the `permission.request` → `lease.granted` collapse in |
| 134 | + §15.4 / §15.5. |
| 135 | +- Document the hybrid Observer/Peer role exemplified by |
| 136 | + `reasoning_streams`. |
| 137 | +- Add a non-normative note in Appendix B that the canonical event |
| 138 | + surface is single-consumer (or recommend a JobMux pattern). |
0 commit comments