Per-Pipe IO Contracts
Every valid validate report carries a pipe_io_contracts map: for each pipe, the typed contract of what it takes in and what it produces. It sits beside the input-form descriptor on the same report, and the two are keyed identically — by the namespaced pipe_ref (domain.code) — over exactly the same set of pipes, PipeSignature placeholders included.
Reach for pipe_io_contracts when you need the schema and the declared contract; reach for input_form when you are building a form and want field kinds, defaults, and choices without reading schemas.
Where it lives
mthds.protocol.pipe_io_contracts— the wire models (PipeInputContract,PipeOutputContract,PipeIOContract,IOMultiplicity,PresenceMarker), declared by the MTHDS standard's Python client and mirroring itspipe-io-contractspage. Pipelex does not declare a second copy of them.pipelex/pipeline/pipe_io_contracts.py— the one public derivation,build_pipe_io_contracts(pipes), which imports and re-exports those models. Because it constructs the standard's own closed shapes, a projection that drifted from the standard fails at derivation rather than on the wire.PipelexValidationReport.pipe_io_contracts(pipelex/pipeline/validation_report.py) — where the artifact travels.pipelex/pipeline/validate_in_process.py— the in-process assembly derives it inside the validation library's window, right besidebuild_input_form. Iterating the same loaded pipes is what makes the two key sets equal by construction.
What an entry carries
Each pipe gets an inputs map and one output.
An input entry carries the concept_ref, the JSON Schema of its content (json_schema), and two facts about the slot as declared:
presence— the authored marker, verbatim:plain,optional(?, the caller may omit it and the pipe handles absence itself), orforce(!, must be provided, and the author asserted so).plainandforceare both required; the distinction is the assertion, which lint and graph surfaces read.multiplicity—single,variable(Concept[], unbounded), orfixed(Concept[N]), withitem_countcarrying the exact N on thefixedarm andnullotherwise.Concept[1]issingle: no list framing.
An output entry carries concept_ref, the same multiplicity / item_count pair, and a two-valued optional — ! is rejected on outputs, so there is nothing three-valued to report. It carries no json_schema: the schema render runs per input only. A consumer that wants the output's payload shape gets it from build output --format schema, not from this contract.
Where the concept identity sits on the schema
The JSON Schema on an input names its concept: title is the concept_ref, description the concept's authored description. On a single slot those sit at the top level; on variable and fixed slots the schema is an array wrap that keeps them on items, and a fixed count adds minItems/maxItems on the wrap itself. A variable list carries no bounds.
{
"type": "array",
"items": { "type": "object", "title": "my_domain.Gadget", "description": "…", "properties": { "…": "…" } },
"minItems": 2,
"maxItems": 2
}
The one concept with no structure class: native.Anything
native.Anything is the only concept that declares no structure class, so its input schema cannot come from a pydantic render. (That is a different question from the three natives which declare no pinned structure — Dynamic, Anything, Composite — and therefore render as unknown in the input-form descriptor.) It publishes the permissive schema: no constraint keywords, only the identity annotations every rendered input schema carries — {"title": "native.Anything", "description": "…"}. Annotation keywords constrain nothing, so this is semantically the empty schema (any JSON value), which is what an untyped vehicle means, while consumers still see the concept's identity instead of a bare {} they would have to special-case. Multiplicity wraps exactly as above: Anything[] is an array of that schema, a fixed count adds the bounds.
The schema states what the concept admits, not what today's input shaper accepts: an Anything slot currently takes a string (shaped into a native.Text stuff) and refuses the other JSON types. Narrowing the published schema to match would be stating a runtime limitation as a contract, so the schema stays permissive and the shaper is what has to catch up.
Which surfaces carry it
The protocol validate operation carries pipe_io_contracts on its report. The Agent CLI does not: pipelex-agent validate emits the verdict, pending_signatures, is_runnable, and its own advisory warnings array, and stops there — the typed contracts are not part of its envelope.
Seeing it
pipelex-dev trace-input-semantics captures the contracts as hop5_pipe_io_contracts.json beside hop5_input_form.json, so an authored fact can be checked on both projections at once — see Tracing Input Semantics. The emitted shapes are pinned by tests/integration/pipelex/pipeline/test_pipe_io_contracts.py.
Related Documentation
- Input-Form Descriptor — the renderer-facing view of the same inputs
- Understanding Optionality — the markers behind
presence - Understanding Multiplicity — the bracket notation behind
multiplicity