Outcome: when Blake carves a bundle, he now writes one small file โ
runs/<run>/steps/1-carve.json โ and his run ends there.
S3 rings a bell on that file, and the broker starts cutting the sixteen members while
Blake is already finished. Nothing else about the carve pass changed: same prompt,
same seat, same one model call, same durable artifact at the end.
A 337-page bundle used to be one Blake run: carve it into sixteen members, then classify, card and segment all sixteen, holding everything in memory and writing a single file at the very end. Three bills came with that:
ADR-180 splits the worker in two. The carver looks at a pile of paper and says "this is sixteen documents". The clerk takes ONE document and registers it properly. They are different jobs, and they are now different runs โ which means member #11 failing is a retry of member #11, not of the bundle.
bundle upload
โโ broker invokes Blake mode="carve"
โโ 1 model call: "where do documents START?"
โโ PUT runs/run-991/steps/1-carve.json โ NEW (this story)
โ โโ S3 fires an event โโ EventBridge โโ SQS โโ the broker
โ โโ cuts 16 PDFs ยท registers members ยท invokes the MAIN clerk
โโ PUT the run's durable artifact โ unchanged
run ENDS. No classify. No card. No segment.
Both PUTs go through the same one function,
agents/_shared/result_delivery.py. That is the point worth staring at:
the story adds a destination, never an egress path. Blake still holds
no S3 key, no AWS SDK, and no bucket name; he holds two one-time signed URLs the broker
minted for him, and CI still fails the build if a second HTTPS PUT site appears
anywhere in agents/.
{
"schema_version": "step_drop.v1",
"run_id": "run-991", "document_id": "doc-42", "bundle_document_id": "doc-42",
"step": "carve", "member": null, "generation": 1,
"drop_id": "doc-42:1:carve",
"main_member": 0,
"expected_members": [0, 1, 2, ... 15], "expected_member_count": 16,
"payload": {
"status": "ok", "bundle_id": "bundle-7", "schemes": ["REGULATOR-ID"],
"members": [
{"member": 0, "role": "main_document", "title": "Complaint", "page_start": 1, "page_end": 34, ...},
{"member": 1, "role": "annexure", "label": "NFRA/2", "page_start": 35, "page_end": 62, ...}
]
},
"proposed_effects": [ {"schema": "document.family.v1", "data": { ...today's carve effect... } } ]
}
| Field | Why it exists |
|---|---|
payload.members |
Blake keeps the main document (head) and the annexures
(members) in separate fields, deliberately โ they are
different things to him. The broker cuts, registers and clerks all of them as ONE
ordered list, and every later drop is addressed member-NN. Somebody
has to declare that numbering once, or sixteen consumers each invent their own. |
main_member |
D18 ordering: the main member's clerk runs FIRST, and the fifteen annexure clerks fan out carrying its card. Declared, never inferred. |
expected_members + count |
"Is this bundle finished?" is answered from this declared set, never from how many queue messages happened to arrive. Delivery order is not semantic order. |
drop_id |
The dedupe key, derived from what the drop is
(document:generation:step) โ never from how it travelled. An SQS
message id changes on every redelivery, so a consumer keyed on one would apply the
same work twice. |
generation |
Re-carve identity. It comes off the envelope the broker minted; the brain never invents one, because only the broker knows this is the second carve of doc-42. |
proposed_effects |
Blake calls no tools and writes nothing (registrar.yaml pins
tools.allowed: []). He proposes; the broker validates and
applies. Today's document.family.v1 rides across verbatim, so the
broker's existing family-persist code keeps working untouched. |
| Failure | What happens instead |
|---|---|
| The envelope for the carve step never arrives. | Fails closed before the run is even registered โ
StepEnvelopeMissingError, with remediation naming
step_envelopes. Discovering it after the model call would burn a
bundle-sized call on an answer with nowhere to land. |
| The drop PUT fails, quietly. | The run's own artifact is written with outcome: "failure" and
failure_stage: "step_drop". A silent drop failure would leave the
broker waiting forever for a bell that never rings. |
| The run failed, but a drop gets written anyway. | A drop is a claim that a step SUCCEEDED, so a failed run writes none โ and logs that it wrote none, with the reason. A branch that exists so nothing happens must say it took itself (the Story-3115 lesson: a guard that silently skipped both branches shipped and ran zero times for eleven merges). |
agents.registrar.invoke span. No new span โ a documented ADR-124
exemption, because the three new span names in the ADR-180 amendment belong to the
broker's consumer surface, not to the brain.BundleFamily,
RegistrarClassifyOutput, ProductionCard) keep their field
names. Every rename lives at the drop boundary only.agents/_shared/step_drop.py the contract: StepEnvelope + StepDropV1 + the drop id
agents/_shared/artifact_runtime.py picks the step's envelope, writes the drop, then the artifact
agents/registrar/carve_drop.py Blake's carve builder โ the numbering lives here
agents/registrar/main.py _STEP_DROP_BY_MODE = {"carve": CARVE_STEP_DROP}
agent_contracts/schemas/step_drop.v1.schema.json the generated artifact under custody
tests/test_step_drop_contract.py exact key ยท fail-closed ยท no-drop-on-failure
agents/registrar/tests/test_registrar_carve_drop.py numbering ยท expected set ยท run ends
adr/180-blake-step-drops), which
is still OPEN. This code is written against it and must not merge before it is
accepted.